Update speaker-mood-music.groovy
[smartapps.git] / official / speaker-mood-music.groovy
1 /**
2  *  Copyright 2015 SmartThings
3  *
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:
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
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.
12  *
13  *  Speaker Mood Music
14  *
15  *  Author: SmartThings
16  *  Date: 2014-02-12
17  */
18 definition(
19     name: "Speaker Mood Music",
20     namespace: "smartthings",
21     author: "SmartThings",
22     description: "Plays a selected song or station.",
23     category: "SmartThings Labs",
24     iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
25     iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
26 )
27
28 preferences {
29         page(name: "mainPage", title: "Play a selected song or station on your Speaker when something happens", nextPage: "chooseTrack", uninstall: true)
30         page(name: "chooseTrack", title: "Select a song", install: true)
31         page(name: "timeIntervalInput", title: "Only during a certain time") {
32                 section {
33                         input "starting", "time", title: "Starting", required: false
34                         input "ending", "time", title: "Ending", required: false
35                 }
36         }
37 }
38
39 private songOptions() {
40
41         // Make sure current selection is in the set
42
43         def options = new LinkedHashSet()
44         if (state.selectedSong?.station) {
45                 options << state.selectedSong.station
46         }
47         else if (state.selectedSong?.description) {
48                 // TODO - Remove eventually? 'description' for backward compatibility
49                 options << state.selectedSong.description
50         }
51
52         // Query for recent tracks
53         def states = sonos.statesSince("trackData", new Date(0), [max:30])
54         def dataMaps = states.collect{it.jsonValue}
55         options.addAll(dataMaps.collect{it.station})
56
57         log.trace "${options.size()} songs in list"
58         options.take(20) as List
59 }
60
61 private saveSelectedSong() {
62         try {
63                 def thisSong = song
64                 log.info "Looking for $thisSong"
65                 def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
66                 log.info "Searching ${songs.size()} records"
67
68                 def data = songs.find {s -> s.station == thisSong}
69                 log.info "Found ${data?.station}"
70                 if (data) {
71                         state.selectedSong = data
72                         log.debug "Selected song = $state.selectedSong"
73                 }
74                 else if (song == state.selectedSong?.station) {
75                         log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
76                 }
77                 else {
78                         log.warn "Selected song '$song' not found"
79                 }
80         }
81         catch (Throwable t) {
82                 log.error t
83         }
84 }
85
86 // input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
87 // input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
88 // input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
89 // input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
90 // input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
91 // input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
92 // input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
93 // input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
94 // input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
95 // input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
96 // input "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
97 // input "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
98 // input "timeOfDay", "time", title: "At a Scheduled Time", required: false
99
100 def mainPage() {
101         dynamicPage(name: "mainPage") {
102                 def anythingSet = anythingSet()
103                 if (anythingSet) {
104                         section("Play music when..."){
105                                 ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
106                                 ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
107                                 ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
108                                 ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
109                                 ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
110                                 ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
111                                 ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
112                                 ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
113                                 ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
114                                 ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
115                                 ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
116                                 ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
117                                 ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
118                         }
119                 }
120
121                 def hideable = anythingSet //|| app.installationState == "COMPLETE"
122                 def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..."
123
124                 section(sectionTitle, hideable: hideable, hidden: true){
125                         ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
126                         ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
127                         ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
128                         ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
129                         ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
130                         ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
131                         ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
132                         ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
133                         ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
134                         ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
135                         ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
136                         ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
137                         ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
138                 }
139                 section {
140                         input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
141                 }
142                 section("More options", hideable: true, hidden: true) {
143                         input "volume", "number", title: "Set the volume", description: "0-100%", required: false
144                         input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
145                         //href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
146                         input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
147                                 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
148                         //if (settings.modes) {
149                 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
150             //}
151                         input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
152                 }
153         }
154 }
155
156 def chooseTrack() {
157         dynamicPage(name: "chooseTrack") {
158                 section{
159                         input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
160                 }
161                 section([mobileOnly:true]) {
162                         label title: "Assign a name", required: false
163                         mode title: "Set for specific mode(s)", required: false
164                 }
165         }
166 }
167
168 private anythingSet() {
169         for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes","timeOfDay"]) {
170                 if (settings[name]) {
171                         return true
172                 }
173         }
174         return false
175 }
176
177 private ifUnset(Map options, String name, String capability) {
178         if (!settings[name]) {
179                 input(options, name, capability)
180         }
181 }
182
183 private ifSet(Map options, String name, String capability) {
184         if (settings[name]) {
185                 input(options, name, capability)
186         }
187 }
188
189 def installed() {
190         log.debug "Installed with settings: ${settings}"
191         subscribeToEvents()
192 }
193
194 def updated() {
195         log.debug "Updated with settings: ${settings}"
196         unsubscribe()
197         unschedule()
198         subscribeToEvents()
199 }
200
201 def subscribeToEvents() {
202         log.trace "subscribeToEvents()"
203         saveSelectedSong()
204
205         subscribe(app, appTouchHandler)
206         subscribe(contact, "contact.open", eventHandler)
207         subscribe(contactClosed, "contact.closed", eventHandler)
208         subscribe(acceleration, "acceleration.active", eventHandler)
209         subscribe(motion, "motion.active", eventHandler)
210         subscribe(mySwitch, "switch.on", eventHandler)
211         subscribe(mySwitchOff, "switch.off", eventHandler)
212         subscribe(arrivalPresence, "presence.present", eventHandler)
213         subscribe(departurePresence, "presence.not present", eventHandler)
214         subscribe(smoke, "smoke.detected", eventHandler)
215         subscribe(smoke, "smoke.tested", eventHandler)
216         subscribe(smoke, "carbonMonoxide.detected", eventHandler)
217         subscribe(water, "water.wet", eventHandler)
218         subscribe(button1, "button.pushed", eventHandler)
219
220         if (triggerModes) {
221                 subscribe(location, modeChangeHandler)
222         }
223
224         if (timeOfDay) {
225                 schedule(timeOfDay, scheduledTimeHandler)
226         }
227 }
228
229 def eventHandler(evt) {
230         if (allOk) {
231                 if (frequency) {
232                         def lastTime = state[frequencyKey(evt)]
233                         if (lastTime == null || now() - lastTime >= frequency * 60000) {
234                                 takeAction(evt)
235                         }
236                 }
237                 else {
238                         takeAction(evt)
239                 }
240         }
241 }
242
243 def modeChangeHandler(evt) {
244         log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
245         if (evt.value in triggerModes) {
246                 eventHandler(evt)
247         }
248 }
249
250 def scheduledTimeHandler() {
251         eventHandler(null)
252 }
253
254 def appTouchHandler(evt) {
255         takeAction(evt)
256 }
257
258 private takeAction(evt) {
259
260         log.info "Playing '$state.selectedSong"
261
262         if (volume != null) {
263                 sonos.stop()
264                 pause(500)
265                 sonos.setLevel(volume)
266                 pause(500)
267         }
268
269         sonos.playTrack(state.selectedSong)
270
271         if (frequency || oncePerDay) {
272                 state[frequencyKey(evt)] = now()
273         }
274         log.trace "Exiting takeAction()"
275 }
276
277 private frequencyKey(evt) {
278         "lastActionTimeStamp"
279 }
280
281 private dayString(Date date) {
282         def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
283         if (location.timeZone) {
284                 df.setTimeZone(location.timeZone)
285         }
286         else {
287                 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
288         }
289         df.format(date)
290 }
291
292 private oncePerDayOk(Long lastTime) {
293         def result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
294         log.trace "oncePerDayOk = $result"
295         result
296 }
297
298 // TODO - centralize somehow
299 private getAllOk() {
300         modeOk && daysOk && timeOk
301 }
302
303 private getModeOk() {
304         def result = !modes || modes.contains(location.mode)
305         log.trace "modeOk = $result"
306         result
307 }
308
309 private getDaysOk() {
310         def result = true
311         if (days) {
312                 def df = new java.text.SimpleDateFormat("EEEE")
313                 if (location.timeZone) {
314                         df.setTimeZone(location.timeZone)
315                 }
316                 else {
317                         df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
318                 }
319                 def day = df.format(new Date())
320                 result = days.contains(day)
321         }
322         log.trace "daysOk = $result"
323         result
324 }
325
326 private getTimeOk() {
327         def result = true
328         if (starting && ending) {
329                 def currTime = now()
330                 def start = timeToday(starting, location?.timeZone).time
331                 def stop = timeToday(ending, location?.timeZone).time
332                 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
333         }
334         log.trace "timeOk = $result"
335         result
336 }
337
338 private hhmm(time, fmt = "h:mm a")
339 {
340         def t = timeToday(time, location.timeZone)
341         def f = new java.text.SimpleDateFormat(fmt)
342         f.setTimeZone(location.timeZone ?: timeZone(time))
343         f.format(t)
344 }
345
346 private timeIntervalLabel()
347 {
348         (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
349 }
350 // TODO - End Centralize
351