21edb437c4529422ab6c078ebd363e21e274ac05
[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 // input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
40 // input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
41 // input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
42 // input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
43 // input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
44 // input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
45 // input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
46 // input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
47 // input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
48 // input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
49 // input "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
50 // input "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
51 // input "timeOfDay", "time", title: "At a Scheduled Time", required: false
52
53 def mainPage() {
54         dynamicPage(name: "mainPage") {
55                 def anythingSet = anythingSet()
56                 if (anythingSet) {
57                         section("Play music when..."){
58                                 ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
59                                 ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
60                                 ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
61                                 ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
62                                 ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
63                                 ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
64                                 ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
65                                 ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
66                                 ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
67                                 ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
68                                 ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
69                                 ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
70                                 ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
71                         }
72                 }
73
74                 def hideable = anythingSet //|| app.installationState == "COMPLETE"
75                 def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..."
76
77                 section(sectionTitle, hideable: hideable, hidden: true){
78                         ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
79                         ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
80                         ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
81                         ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
82                         ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
83                         ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
84                         ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
85                         ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
86                         ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
87                         ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
88                         ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
89                         ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
90                         ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
91                 }
92                 section {
93                         input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
94                 }
95                 section("More options", hideable: true, hidden: true) {
96                         input "volume", "number", title: "Set the volume", description: "0-100%", required: false
97                         input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
98                         //href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
99                         input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
100                                 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
101                         //if (settings.modes) {
102                 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
103             //}
104                         input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
105                 }
106         }
107 }
108
109 def chooseTrack() {
110         dynamicPage(name: "chooseTrack") {
111                 section{
112                         input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
113                 }
114                 section([mobileOnly:true]) {
115                         label title: "Assign a name", required: false
116                         mode title: "Set for specific mode(s)", required: false
117                 }
118         }
119 }
120
121 private songOptions() {
122
123         // Make sure current selection is in the set
124
125         def options = new LinkedHashSet()
126         if (state.selectedSong?.station) {
127                 options << state.selectedSong.station
128         }
129         else if (state.selectedSong?.description) {
130                 // TODO - Remove eventually? 'description' for backward compatibility
131                 options << state.selectedSong.description
132         }
133
134         // Query for recent tracks
135         def states = sonos.statesSince("trackData", new Date(0), [max:30])
136         def dataMaps = states.collect{it.jsonValue}
137         options.addAll(dataMaps.collect{it.station})
138
139         log.trace "${options.size()} songs in list"
140         options.take(20) as List
141 }
142
143 private saveSelectedSong() {
144         try {
145                 def thisSong = song
146                 log.info "Looking for $thisSong"
147                 def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
148                 log.info "Searching ${songs.size()} records"
149
150                 def data = songs.find {s -> s.station == thisSong}
151                 log.info "Found ${data?.station}"
152                 if (data) {
153                         state.selectedSong = data
154                         log.debug "Selected song = $state.selectedSong"
155                 }
156                 else if (song == state.selectedSong?.station) {
157                         log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
158                 }
159                 else {
160                         log.warn "Selected song '$song' not found"
161                 }
162         }
163         catch (Throwable t) {
164                 log.error t
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