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