2 * Copyright 2015 SmartThings
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:
7 * http://www.apache.org/licenses/LICENSE-2.0
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.
13 * Speaker Custom Message
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"
29 page(name: "timeIntervalInput", title: "Only during a certain time") {
31 input "starting", "time", title: "Starting", required: false
32 input "ending", "time", title: "Ending", required: false
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")
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
54 dynamicPage(name: "mainPage") {
55 def anythingSet = 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
73 def hideable = anythingSet //|| app.installationState == "COMPLETE"
74 def sectionTitle = anythingSet ? "Select additional triggers" : "Play message when..."
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
92 input "actionType", "enum", title: "Action?", required: true, defaultValue: "Bell 1", options: [
98 "The mail has arrived",
101 "Smartthings detected a flood",
102 "Smartthings detected smoke",
103 "Someone is arriving",
106 input "message","text",title:"Play this message", required:false, multiple: false
109 input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
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"
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
123 input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
125 section([mobileOnly:true]) {
126 label title: "Assign a name", required: false
127 mode title: "Set for specific mode(s)", required: false
133 dynamicPage(name: "chooseTrack") {
135 input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
140 private songOptions() {
142 // Make sure current selection is in the set
144 def options = new LinkedHashSet()
145 if (state.selectedSong?.station) {
146 options << state.selectedSong.station
148 else if (state.selectedSong?.description) {
149 // TODO - Remove eventually? 'description' for backward compatibility
150 options << state.selectedSong.description
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})
158 log.trace "${options.size()} songs in list"
159 options.take(20) as List
162 private saveSelectedSong() {
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"
169 def data = songs.find {s -> s.station == thisSong}
170 log.info "Found ${data?.station}"
172 state.selectedSong = data
173 log.debug "Selected song = $state.selectedSong"
175 else if (song == state.selectedSong?.station) {
176 log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
179 log.warn "Selected song '$song' not found"
182 catch (Throwable t) {
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]) {
196 private ifUnset(Map options, String name, String capability) {
197 if (!settings[name]) {
198 input(options, name, capability)
202 private ifSet(Map options, String name, String capability) {
203 if (settings[name]) {
204 input(options, name, capability)
209 log.debug "Installed with settings: ${settings}"
214 log.debug "Updated with settings: ${settings}"
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)
237 subscribe(location, modeChangeHandler)
241 schedule(timeOfDay, scheduledTimeHandler)
251 def eventHandler(evt) {
252 log.trace "eventHandler($evt?.name: $evt?.value)"
255 def lastTime = state[frequencyKey(evt)]
256 if (oncePerDayOk(lastTime)) {
258 if (lastTime == null || now() - lastTime >= frequency * 60000) {
262 log.debug "Not taking action because $frequency minutes have not elapsed since last action"
270 log.debug "Not taking action because it was already taken today"
274 def modeChangeHandler(evt) {
275 log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
276 if (evt.value in triggerModes) {
281 def scheduledTimeHandler() {
285 def appTouchHandler(evt) {
289 private takeAction(evt) {
291 log.trace "takeAction()"
294 //sonos.playSoundAndTrack(state.sound.uri, state.sound.duration, state.selectedSong, volume)
296 else if (resumePlaying){
297 //sonos.playTrackAndResume(state.sound.uri, state.sound.duration, volume)
300 //sonos.playTrackAndRestore(state.sound.uri, state.sound.duration, volume)
303 if (frequency || oncePerDay) {
304 state[frequencyKey(evt)] = now()
306 log.trace "Exiting takeAction()"
309 private frequencyKey(evt) {
310 "lastActionTimeStamp"
313 private dayString(Date date) {
314 def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
315 if (location.timeZone) {
316 df.setTimeZone(location.timeZone)
319 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
324 private oncePerDayOk(Long lastTime) {
327 result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
328 log.trace "oncePerDayOk = $result"
333 // TODO - centralize somehow
335 modeOk && daysOk && timeOk
338 private getModeOk() {
339 def result = !modes || modes.contains(location.mode)
340 log.trace "modeOk = $result"
344 private getDaysOk() {
347 def df = new java.text.SimpleDateFormat("EEEE")
348 if (location.timeZone) {
349 df.setTimeZone(location.timeZone)
352 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
354 def day = df.format(new Date())
355 result = days.contains(day)
357 log.trace "daysOk = $result"
361 private getTimeOk() {
363 if (starting && ending) {
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
369 log.trace "timeOk = $result"
373 private hhmm(time, fmt = "h:mm a")
375 def t = timeToday(time, location.timeZone)
376 def f = new java.text.SimpleDateFormat(fmt)
377 f.setTimeZone(location.timeZone ?: timeZone(time))
381 private getTimeLabel()
383 (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
385 // TODO - End Centralize
388 switch ( actionType) {
390 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]
393 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell2.mp3", duration: "10"]
396 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/dogs.mp3", duration: "10"]
399 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/alarm.mp3", duration: "17"]
401 case "The mail has arrived":
402 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/the+mail+has+arrived.mp3", duration: "1"]
404 case "A door opened":
405 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/a+door+opened.mp3", duration: "1"]
407 case "There is motion":
408 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/there+is+motion.mp3", duration: "1"]
410 case "Smartthings detected a flood":
411 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+a+flood.mp3", duration: "2"]
413 case "Smartthings detected smoke":
414 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+smoke.mp3", duration: "1"]
416 case "Someone is arriving":
417 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/someone+is+arriving.mp3", duration: "1"]
420 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/piano2.mp3", duration: "10"]
423 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"]
425 case "Custom Message":
427 //state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed)
430 //state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App")
434 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]