Update groveStreams.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 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
101
102 def mainPage() {
103         dynamicPage(name: "mainPage") {
104                 def anythingSet = anythingSet()
105                 if (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
120                         }
121                 }
122
123                 def hideable = anythingSet //|| app.installationState == "COMPLETE"
124                 def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..."
125
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
140                 }
141                 section {
142                         input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
143                 }
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
152             //}
153                         input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
154                 }
155         }
156 }
157
158 def chooseTrack() {
159         dynamicPage(name: "chooseTrack") {
160                 section{
161                         input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
162                 }
163                 section([mobileOnly:true]) {
164                         label title: "Assign a name", required: false
165                         mode title: "Set for specific mode(s)", required: false
166                 }
167         }
168 }
169
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]) {
173                         return true
174                 }
175         }
176         return false
177 }
178
179 private ifUnset(Map options, String name, String capability) {
180         if (!settings[name]) {
181                 input(options, name, capability)
182         }
183 }
184
185 private ifSet(Map options, String name, String capability) {
186         if (settings[name]) {
187                 input(options, name, capability)
188         }
189 }
190
191 def installed() {
192         log.debug "Installed with settings: ${settings}"
193         subscribeToEvents()
194 }
195
196 def updated() {
197         log.debug "Updated with settings: ${settings}"
198         unsubscribe()
199         unschedule()
200         subscribeToEvents()
201 }
202
203 def subscribeToEvents() {
204         log.trace "subscribeToEvents()"
205         saveSelectedSong()
206
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)
221
222         if (triggerModes) {
223                 subscribe(location, modeChangeHandler)
224         }
225
226         if (timeOfDay) {
227                 schedule(timeOfDay, scheduledTimeHandler)
228         }
229 }
230
231 def eventHandler(evt) {
232         if (allOk) {
233                 if (frequency) {
234                         def lastTime = state[frequencyKey(evt)]
235                         if (lastTime == null || now() - lastTime >= frequency * 60000) {
236                                 takeAction(evt)
237                         }
238                 }
239                 else {
240                         takeAction(evt)
241                 }
242         }
243 }
244
245 def modeChangeHandler(evt) {
246         log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
247         if (evt.value in triggerModes) {
248                 eventHandler(evt)
249         }
250 }
251
252 def scheduledTimeHandler() {
253         eventHandler(null)
254 }
255
256 def appTouchHandler(evt) {
257         takeAction(evt)
258 }
259
260 private takeAction(evt) {
261
262         log.info "Playing '$state.selectedSong"
263
264         if (volume != null) {
265                 sonos.stop()
266                 pause(500)
267                 sonos.setLevel(volume)
268                 pause(500)
269         }
270
271         sonos.playTrack(state.selectedSong)
272
273         if (frequency || oncePerDay) {
274                 state[frequencyKey(evt)] = now()
275         }
276         log.trace "Exiting takeAction()"
277 }
278
279 private frequencyKey(evt) {
280         "lastActionTimeStamp"
281 }
282
283 private dayString(Date date) {
284         def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
285         if (location.timeZone) {
286                 df.setTimeZone(location.timeZone)
287         }
288         else {
289                 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
290         }
291         df.format(date)
292 }
293
294 private oncePerDayOk(Long lastTime) {
295         def result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
296         log.trace "oncePerDayOk = $result"
297         result
298 }
299
300 // TODO - centralize somehow
301 private getAllOk() {
302         modeOk && daysOk && timeOk
303 }
304
305 private getModeOk() {
306         def result = !modes || modes.contains(location.mode)
307         log.trace "modeOk = $result"
308         result
309 }
310
311 private getDaysOk() {
312         def result = true
313         if (days) {
314                 def df = new java.text.SimpleDateFormat("EEEE")
315                 if (location.timeZone) {
316                         df.setTimeZone(location.timeZone)
317                 }
318                 else {
319                         df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
320                 }
321                 def day = df.format(new Date())
322                 result = days.contains(day)
323         }
324         log.trace "daysOk = $result"
325         result
326 }
327
328 private getTimeOk() {
329         def result = true
330         if (starting && ending) {
331                 def currTime = now()
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
335         }
336         log.trace "timeOk = $result"
337         result
338 }
339
340 private hhmm(time, fmt = "h:mm a")
341 {
342         def t = timeToday(time, location.timeZone)
343         def f = new java.text.SimpleDateFormat(fmt)
344         f.setTimeZone(location.timeZone ?: timeZone(time))
345         f.format(t)
346 }
347
348 private timeIntervalLabel()
349 {
350         (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
351 }
352 // TODO - End Centralize
353