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: "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") {
33 input "starting", "time", title: "Starting", required: false
34 input "ending", "time", title: "Ending", required: false
40 dynamicPage(name: "mainPage") {
41 def anythingSet = 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
59 def hideable = anythingSet || app.installationState == "COMPLETE"
60 def sectionTitle = anythingSet ? "Select additional triggers" : "Play message when..."
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
78 input "actionType", "enum", title: "Action?", required: true, defaultValue: "Bell 1", options: [
84 "The mail has arrived",
87 "Smartthings detected a flood",
88 "Smartthings detected smoke",
89 "Someone is arriving",
92 input "message","text",title:"Play this message", required:false, multiple: false
95 input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
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"
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
109 input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
111 section([mobileOnly:true]) {
112 label title: "Assign a name", required: false
113 mode title: "Set for specific mode(s)", required: false
119 dynamicPage(name: "chooseTrack") {
121 input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
126 private songOptions() {
128 // Make sure current selection is in the set
130 def options = new LinkedHashSet()
131 if (state.selectedSong?.station) {
132 options << state.selectedSong.station
134 else if (state.selectedSong?.description) {
135 // TODO - Remove eventually? 'description' for backward compatibility
136 options << state.selectedSong.description
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})
144 log.trace "${options.size()} songs in list"
145 options.take(20) as List
148 private saveSelectedSong() {
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"
155 def data = songs.find {s -> s.station == thisSong}
156 log.info "Found ${data?.station}"
158 state.selectedSong = data
159 log.debug "Selected song = $state.selectedSong"
161 else if (song == state.selectedSong?.station) {
162 log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
165 log.warn "Selected song '$song' not found"
168 catch (Throwable t) {
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]) {
182 private ifUnset(Map options, String name, String capability) {
183 if (!settings[name]) {
184 input(options, name, capability)
188 private ifSet(Map options, String name, String capability) {
189 if (settings[name]) {
190 input(options, name, capability)
195 log.debug "Installed with settings: ${settings}"
200 log.debug "Updated with settings: ${settings}"
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)
223 subscribe(location, modeChangeHandler)
227 schedule(timeOfDay, scheduledTimeHandler)
237 def eventHandler(evt) {
238 log.trace "eventHandler($evt?.name: $evt?.value)"
241 def lastTime = state[frequencyKey(evt)]
242 if (oncePerDayOk(lastTime)) {
244 if (lastTime == null || now() - lastTime >= frequency * 60000) {
248 log.debug "Not taking action because $frequency minutes have not elapsed since last action"
256 log.debug "Not taking action because it was already taken today"
260 def modeChangeHandler(evt) {
261 log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
262 if (evt.value in triggerModes) {
267 def scheduledTimeHandler() {
271 def appTouchHandler(evt) {
275 private takeAction(evt) {
277 log.trace "takeAction()"
280 sonos.playSoundAndTrack(state.sound.uri, state.sound.duration, state.selectedSong, volume)
282 else if (resumePlaying){
283 sonos.playTrackAndResume(state.sound.uri, state.sound.duration, volume)
286 sonos.playTrackAndRestore(state.sound.uri, state.sound.duration, volume)
289 if (frequency || oncePerDay) {
290 state[frequencyKey(evt)] = now()
292 log.trace "Exiting takeAction()"
295 private frequencyKey(evt) {
296 "lastActionTimeStamp"
299 private dayString(Date date) {
300 def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
301 if (location.timeZone) {
302 df.setTimeZone(location.timeZone)
305 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
310 private oncePerDayOk(Long lastTime) {
313 result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
314 log.trace "oncePerDayOk = $result"
319 // TODO - centralize somehow
321 modeOk && daysOk && timeOk
324 private getModeOk() {
325 def result = !modes || modes.contains(location.mode)
326 log.trace "modeOk = $result"
330 private getDaysOk() {
333 def df = new java.text.SimpleDateFormat("EEEE")
334 if (location.timeZone) {
335 df.setTimeZone(location.timeZone)
338 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
340 def day = df.format(new Date())
341 result = days.contains(day)
343 log.trace "daysOk = $result"
347 private getTimeOk() {
349 if (starting && ending) {
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
355 log.trace "timeOk = $result"
359 private hhmm(time, fmt = "h:mm a")
361 def t = timeToday(time, location.timeZone)
362 def f = new java.text.SimpleDateFormat(fmt)
363 f.setTimeZone(location.timeZone ?: timeZone(time))
367 private getTimeLabel()
369 (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
371 // TODO - End Centralize
374 switch ( actionType) {
376 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]
379 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell2.mp3", duration: "10"]
382 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/dogs.mp3", duration: "10"]
385 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/alarm.mp3", duration: "17"]
387 case "The mail has arrived":
388 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/the+mail+has+arrived.mp3", duration: "1"]
390 case "A door opened":
391 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/a+door+opened.mp3", duration: "1"]
393 case "There is motion":
394 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/there+is+motion.mp3", duration: "1"]
396 case "Smartthings detected a flood":
397 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+a+flood.mp3", duration: "2"]
399 case "Smartthings detected smoke":
400 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+smoke.mp3", duration: "1"]
402 case "Someone is arriving":
403 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/someone+is+arriving.mp3", duration: "1"]
406 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/piano2.mp3", duration: "10"]
409 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"]
411 case "Custom Message":
413 state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed)
416 state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App")
420 state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]