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