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