2 * Copyright 2015 SmartThings
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at:
7 * http://www.apache.org/licenses/LICENSE-2.0
9 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11 * for the specific language governing permissions and limitations under the License.
20 private songOptions() {
22 // Make sure current selection is in the set
24 def options = new LinkedHashSet()
25 if (state.selectedSong?.station) {
26 options << state.selectedSong.station
28 else if (state.selectedSong?.description) {
29 // TODO - Remove eventually? 'description' for backward compatibility
30 options << state.selectedSong.description
33 // Query for recent tracks
34 def states = sonos.statesSince("trackData", new Date(0), [max:30])
35 def dataMaps = states.collect{it.jsonValue}
36 options.addAll(dataMaps.collect{it.station})
38 log.trace "${options.size()} songs in list"
39 options.take(20) as List
42 private saveSelectedSong() {
45 log.info "Looking for $thisSong"
46 def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
47 log.info "Searching ${songs.size()} records"
49 def data = songs.find {s -> s.station == thisSong}
50 log.info "Found ${data?.station}"
52 state.selectedSong = data
53 log.debug "Selected song = $state.selectedSong"
55 else if (song == state.selectedSong?.station) {
56 log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
59 log.warn "Selected song '$song' not found"
68 name: "Speaker Mood Music",
69 namespace: "smartthings",
70 author: "SmartThings",
71 description: "Plays a selected song or station.",
72 category: "SmartThings Labs",
73 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
74 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
78 page(name: "mainPage", title: "Play a selected song or station on your Speaker when something happens", nextPage: "chooseTrack", uninstall: true)
79 page(name: "chooseTrack", title: "Select a song", install: true)
80 page(name: "timeIntervalInput", title: "Only during a certain time") {
82 input "starting", "time", title: "Starting", required: false
83 input "ending", "time", title: "Ending", required: false
88 // input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
89 // input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
90 // input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
91 // input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
92 // input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
93 // input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
94 // input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
95 // input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
96 // input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
97 // input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
98 // input "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
99 // input "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
100 // input "timeOfDay", "time", title: "At a Scheduled Time", required: false
103 dynamicPage(name: "mainPage") {
104 def anythingSet = anythingSet()
106 section("Play music when..."){
107 ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
108 ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
109 ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
110 ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
111 ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
112 ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
113 ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
114 ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
115 ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
116 ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
117 ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
118 ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
119 ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
123 def hideable = anythingSet //|| app.installationState == "COMPLETE"
124 def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..."
126 section(sectionTitle, hideable: hideable, hidden: true){
127 ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
128 ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
129 ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
130 ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
131 ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
132 ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
133 ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
134 ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
135 ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
136 ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
137 ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
138 ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
139 ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
142 input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
144 section("More options", hideable: true, hidden: true) {
145 input "volume", "number", title: "Set the volume", description: "0-100%", required: false
146 input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
147 //href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
148 input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
149 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
150 //if (settings.modes) {
151 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
153 input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
159 dynamicPage(name: "chooseTrack") {
161 input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
163 section([mobileOnly:true]) {
164 label title: "Assign a name", required: false
165 mode title: "Set for specific mode(s)", required: false
170 private anythingSet() {
171 for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes","timeOfDay"]) {
172 if (settings[name]) {
179 private ifUnset(Map options, String name, String capability) {
180 if (!settings[name]) {
181 input(options, name, capability)
185 private ifSet(Map options, String name, String capability) {
186 if (settings[name]) {
187 input(options, name, capability)
192 log.debug "Installed with settings: ${settings}"
197 log.debug "Updated with settings: ${settings}"
203 def subscribeToEvents() {
204 log.trace "subscribeToEvents()"
207 subscribe(app, appTouchHandler)
208 subscribe(contact, "contact.open", eventHandler)
209 subscribe(contactClosed, "contact.closed", eventHandler)
210 subscribe(acceleration, "acceleration.active", eventHandler)
211 subscribe(motion, "motion.active", eventHandler)
212 subscribe(mySwitch, "switch.on", eventHandler)
213 subscribe(mySwitchOff, "switch.off", eventHandler)
214 subscribe(arrivalPresence, "presence.present", eventHandler)
215 subscribe(departurePresence, "presence.not present", eventHandler)
216 subscribe(smoke, "smoke.detected", eventHandler)
217 subscribe(smoke, "smoke.tested", eventHandler)
218 subscribe(smoke, "carbonMonoxide.detected", eventHandler)
219 subscribe(water, "water.wet", eventHandler)
220 subscribe(button1, "button.pushed", eventHandler)
223 subscribe(location, modeChangeHandler)
227 schedule(timeOfDay, scheduledTimeHandler)
231 def eventHandler(evt) {
234 def lastTime = state[frequencyKey(evt)]
235 if (lastTime == null || now() - lastTime >= frequency * 60000) {
245 def modeChangeHandler(evt) {
246 log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
247 if (evt.value in triggerModes) {
252 def scheduledTimeHandler() {
256 def appTouchHandler(evt) {
260 private takeAction(evt) {
262 log.info "Playing '$state.selectedSong"
264 if (volume != null) {
267 sonos.setLevel(volume)
271 sonos.playTrack(state.selectedSong)
273 if (frequency || oncePerDay) {
274 state[frequencyKey(evt)] = now()
276 log.trace "Exiting takeAction()"
279 private frequencyKey(evt) {
280 "lastActionTimeStamp"
283 private dayString(Date date) {
284 def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
285 if (location.timeZone) {
286 df.setTimeZone(location.timeZone)
289 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
294 private oncePerDayOk(Long lastTime) {
295 def result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
296 log.trace "oncePerDayOk = $result"
300 // TODO - centralize somehow
302 modeOk && daysOk && timeOk
305 private getModeOk() {
306 def result = !modes || modes.contains(location.mode)
307 log.trace "modeOk = $result"
311 private getDaysOk() {
314 def df = new java.text.SimpleDateFormat("EEEE")
315 if (location.timeZone) {
316 df.setTimeZone(location.timeZone)
319 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
321 def day = df.format(new Date())
322 result = days.contains(day)
324 log.trace "daysOk = $result"
328 private getTimeOk() {
330 if (starting && ending) {
332 def start = timeToday(starting, location?.timeZone).time
333 def stop = timeToday(ending, location?.timeZone).time
334 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
336 log.trace "timeOk = $result"
340 private hhmm(time, fmt = "h:mm a")
342 def t = timeToday(time, location.timeZone)
343 def f = new java.text.SimpleDateFormat(fmt)
344 f.setTimeZone(location.timeZone ?: timeZone(time))
348 private timeIntervalLabel()
350 (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
352 // TODO - End Centralize