Update garage-switch.groovy
[smartapps.git] / official / speaker-notify-with-sound.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 Custom Message
14  *
15  *  Author: SmartThings
16  *  Date: 2014-1-29
17  */
18 definition(
19         name: "Speaker Notify with Sound",
20         namespace: "smartthings",
21         author: "SmartThings",
22         description: "Play a sound or custom message through your Speaker when the mode changes or other events occur.",
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 message on your Speaker when something happens", install: true, uninstall: true)
30         page(name: "chooseTrack", title: "Select a song or station")
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 def mainPage() {
40         dynamicPage(name: "mainPage") {
41                 def anythingSet = anythingSet()
42                 if (anythingSet) {
43                         section("Play message when"){
44                                 ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
45                                 ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
46                                 ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
47                                 ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
48                                 ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
49                                 ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
50                                 ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
51                                 ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
52                                 ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
53                                 ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
54                                 ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
55                                 ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
56                                 ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
57                         }
58                 }
59                 def hideable = anythingSet || app.installationState == "COMPLETE"
60                 def sectionTitle = anythingSet ? "Select additional triggers" : "Play message when..."
61
62                 section(sectionTitle, hideable: hideable, hidden: true){
63                         ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
64                         ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
65                         ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
66                         ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
67                         ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
68                         ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
69                         ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
70                         ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
71                         ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
72                         ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
73                         ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
74                         ifUnset "triggerModes", "mode", title: "System Changes Mode", description: "Select mode(s)", required: false, multiple: true
75                         ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
76                 }
77                 section{
78                         input "actionType", "enum", title: "Action?", required: true, defaultValue: "Bell 1", options: [
79                                 "Custom Message",
80                                 "Bell 1",
81                                 "Bell 2",
82                                 "Dogs Barking",
83                                 "Fire Alarm",
84                                 "The mail has arrived",
85                                 "A door opened",
86                                 "There is motion",
87                                 "Smartthings detected a flood",
88                                 "Smartthings detected smoke",
89                                 "Someone is arriving",
90                                 "Piano",
91                                 "Lightsaber"]
92                         input "message","text",title:"Play this message", required:false, multiple: false
93                 }
94                 section {
95                         input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
96                 }
97                 section("More options", hideable: true, hidden: true) {
98                         input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true
99                         href "chooseTrack", title: "Or play this music or radio station", description: song ? state.selectedSong?.station : "Tap to set", state: song ? "complete" : "incomplete"
100
101                         input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false
102                         input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
103                         href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
104                         input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
105                                 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
106                         if (settings.modes) {
107                 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
108             }
109                         input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
110                 }
111                 section([mobileOnly:true]) {
112                         label title: "Assign a name", required: false
113                         mode title: "Set for specific mode(s)", required: false
114                 }
115         }
116 }
117
118 def chooseTrack() {
119         dynamicPage(name: "chooseTrack") {
120                 section{
121                         input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
122                 }
123         }
124 }
125
126 private songOptions() {
127
128         // Make sure current selection is in the set
129
130         def options = new LinkedHashSet()
131         if (state.selectedSong?.station) {
132                 options << state.selectedSong.station
133         }
134         else if (state.selectedSong?.description) {
135                 // TODO - Remove eventually? 'description' for backward compatibility
136                 options << state.selectedSong.description
137         }
138
139         // Query for recent tracks
140         def states = sonos.statesSince("trackData", new Date(0), [max:30])
141         def dataMaps = states.collect{it.jsonValue}
142         options.addAll(dataMaps.collect{it.station})
143
144         log.trace "${options.size()} songs in list"
145         options.take(20) as List
146 }
147
148 private saveSelectedSong() {
149         try {
150                 def thisSong = song
151                 log.info "Looking for $thisSong"
152                 def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
153                 log.info "Searching ${songs.size()} records"
154
155                 def data = songs.find {s -> s.station == thisSong}
156                 log.info "Found ${data?.station}"
157                 if (data) {
158                         state.selectedSong = data
159                         log.debug "Selected song = $state.selectedSong"
160                 }
161                 else if (song == state.selectedSong?.station) {
162                         log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
163                 }
164                 else {
165                         log.warn "Selected song '$song' not found"
166                 }
167         }
168         catch (Throwable t) {
169                 log.error t
170         }
171 }
172
173 private anythingSet() {
174         for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes","timeOfDay"]) {
175                 if (settings[name]) {
176                         return true
177                 }
178         }
179         return false
180 }
181
182 private ifUnset(Map options, String name, String capability) {
183         if (!settings[name]) {
184                 input(options, name, capability)
185         }
186 }
187
188 private ifSet(Map options, String name, String capability) {
189         if (settings[name]) {
190                 input(options, name, capability)
191         }
192 }
193
194 def installed() {
195         log.debug "Installed with settings: ${settings}"
196         subscribeToEvents()
197 }
198
199 def updated() {
200         log.debug "Updated with settings: ${settings}"
201         unsubscribe()
202         unschedule()
203         subscribeToEvents()
204 }
205
206 def subscribeToEvents() {
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         if (song) {
231                 saveSelectedSong()
232         }
233
234         loadText()
235 }
236
237 def eventHandler(evt) {
238         log.trace "eventHandler($evt?.name: $evt?.value)"
239         if (allOk) {
240                 log.trace "allOk"
241                 def lastTime = state[frequencyKey(evt)]
242                 if (oncePerDayOk(lastTime)) {
243                         if (frequency) {
244                                 if (lastTime == null || now() - lastTime >= frequency * 60000) {
245                                         takeAction(evt)
246                                 }
247                                 else {
248                                         log.debug "Not taking action because $frequency minutes have not elapsed since last action"
249                                 }
250                         }
251                         else {
252                                 takeAction(evt)
253                         }
254                 }
255                 else {
256                         log.debug "Not taking action because it was already taken today"
257                 }
258         }
259 }
260 def modeChangeHandler(evt) {
261         log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
262         if (evt.value in triggerModes) {
263                 eventHandler(evt)
264         }
265 }
266
267 def scheduledTimeHandler() {
268         eventHandler(null)
269 }
270
271 def appTouchHandler(evt) {
272         takeAction(evt)
273 }
274
275 private takeAction(evt) {
276
277         log.trace "takeAction()"
278
279         if (song) {
280                 sonos.playSoundAndTrack(state.sound.uri, state.sound.duration, state.selectedSong, volume)
281         }
282         else if (resumePlaying){
283                 sonos.playTrackAndResume(state.sound.uri, state.sound.duration, volume)
284         }
285         else {
286                 sonos.playTrackAndRestore(state.sound.uri, state.sound.duration, volume)
287         }
288
289         if (frequency || oncePerDay) {
290                 state[frequencyKey(evt)] = now()
291         }
292         log.trace "Exiting takeAction()"
293 }
294
295 private frequencyKey(evt) {
296         "lastActionTimeStamp"
297 }
298
299 private dayString(Date date) {
300         def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
301         if (location.timeZone) {
302                 df.setTimeZone(location.timeZone)
303         }
304         else {
305                 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
306         }
307         df.format(date)
308 }
309
310 private oncePerDayOk(Long lastTime) {
311         def result = true
312         if (oncePerDay) {
313                 result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
314                 log.trace "oncePerDayOk = $result"
315         }
316         result
317 }
318
319 // TODO - centralize somehow
320 private getAllOk() {
321         modeOk && daysOk && timeOk
322 }
323
324 private getModeOk() {
325         def result = !modes || modes.contains(location.mode)
326         log.trace "modeOk = $result"
327         result
328 }
329
330 private getDaysOk() {
331         def result = true
332         if (days) {
333                 def df = new java.text.SimpleDateFormat("EEEE")
334                 if (location.timeZone) {
335                         df.setTimeZone(location.timeZone)
336                 }
337                 else {
338                         df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
339                 }
340                 def day = df.format(new Date())
341                 result = days.contains(day)
342         }
343         log.trace "daysOk = $result"
344         result
345 }
346
347 private getTimeOk() {
348         def result = true
349         if (starting && ending) {
350                 def currTime = now()
351                 def start = timeToday(starting, location?.timeZone).time
352                 def stop = timeToday(ending, location?.timeZone).time
353                 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
354         }
355         log.trace "timeOk = $result"
356         result
357 }
358
359 private hhmm(time, fmt = "h:mm a")
360 {
361         def t = timeToday(time, location.timeZone)
362         def f = new java.text.SimpleDateFormat(fmt)
363         f.setTimeZone(location.timeZone ?: timeZone(time))
364         f.format(t)
365 }
366
367 private getTimeLabel()
368 {
369         (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
370 }
371 // TODO - End Centralize
372
373 private loadText() {
374         switch ( actionType) {
375                 case "Bell 1":
376                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]
377                         break;
378                 case "Bell 2":
379                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell2.mp3", duration: "10"]
380                         break;
381                 case "Dogs Barking":
382                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/dogs.mp3", duration: "10"]
383                         break;
384                 case "Fire Alarm":
385                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/alarm.mp3", duration: "17"]
386                         break;
387                 case "The mail has arrived":
388                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/the+mail+has+arrived.mp3", duration: "1"]
389                         break;
390                 case "A door opened":
391                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/a+door+opened.mp3", duration: "1"]
392                         break;
393                 case "There is motion":
394                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/there+is+motion.mp3", duration: "1"]
395                         break;
396                 case "Smartthings detected a flood":
397                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+a+flood.mp3", duration: "2"]
398                         break;
399                 case "Smartthings detected smoke":
400                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+smoke.mp3", duration: "1"]
401                         break;
402                 case "Someone is arriving":
403                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/someone+is+arriving.mp3", duration: "1"]
404                         break;
405                 case "Piano":
406                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/piano2.mp3", duration: "10"]
407                         break;
408                 case "Lightsaber":
409                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"]
410                         break;
411                 case "Custom Message":
412                         if (message) {
413                                 state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed)
414                         }
415                         else {
416                                 state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App")
417                         }
418                         break;
419                 default:
420                         state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]
421                         break;
422         }
423 }