/** * Copyright 2015 SmartThings * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * Speaker Custom Message * * Author: SmartThings * Date: 2014-1-29 */ definition( name: "Speaker Notify with Sound", namespace: "smartthings", author: "SmartThings", description: "Play a sound or custom message through your Speaker when the mode changes or other events occur.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" ) preferences { page(name: "timeIntervalInput", title: "Only during a certain time") { section { input "starting", "time", title: "Starting", required: false input "ending", "time", title: "Ending", required: false } } page(name: "mainPage", title: "Play a message on your Speaker when something happens", install: true, uninstall: true) page(name: "chooseTrack", title: "Select a song or station") } // input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true // input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true // input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true // input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true // input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true // input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true // input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true // input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true // input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true // input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true // input "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production // input "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true // input "timeOfDay", "time", title: "At a Scheduled Time", required: false def mainPage() { dynamicPage(name: "mainPage") { def anythingSet = anythingSet() if (anythingSet) { section("Play message when"){ ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false } } def hideable = anythingSet //|| app.installationState == "COMPLETE" def sectionTitle = anythingSet ? "Select additional triggers" : "Play message when..." section(sectionTitle, hideable: hideable, hidden: true){ ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production ifUnset "triggerModes", "mode", title: "System Changes Mode", description: "Select mode(s)", required: false, multiple: true ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false } section{ input "actionType", "enum", title: "Action?", required: true, defaultValue: "Bell 1", options: [ "Custom Message", "Bell 1", "Bell 2", "Dogs Barking", "Fire Alarm", "The mail has arrived", "A door opened", "There is motion", "Smartthings detected a flood", "Smartthings detected smoke", "Someone is arriving", "Piano", "Lightsaber"] input "message","text",title:"Play this message", required:false, multiple: false } section { input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true } section("More options", hideable: true, hidden: true) { input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true //href "chooseTrack", title: "Or play this music or radio station", description: song ? state.selectedSong?.station : "Tap to set", state: song ? "complete" : "incomplete" input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] //if (settings.modes) { input "modes", "mode", title: "Only when mode is", multiple: true, required: false //} input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false } section([mobileOnly:true]) { label title: "Assign a name", required: false mode title: "Set for specific mode(s)", required: false } } } def chooseTrack() { dynamicPage(name: "chooseTrack") { section{ input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions() } } } private songOptions() { // Make sure current selection is in the set def options = new LinkedHashSet() if (state.selectedSong?.station) { options << state.selectedSong.station } else if (state.selectedSong?.description) { // TODO - Remove eventually? 'description' for backward compatibility options << state.selectedSong.description } // Query for recent tracks def states = sonos.statesSince("trackData", new Date(0), [max:30]) def dataMaps = states.collect{it.jsonValue} options.addAll(dataMaps.collect{it.station}) log.trace "${options.size()} songs in list" options.take(20) as List } private saveSelectedSong() { try { def thisSong = song log.info "Looking for $thisSong" def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue} log.info "Searching ${songs.size()} records" def data = songs.find {s -> s.station == thisSong} log.info "Found ${data?.station}" if (data) { state.selectedSong = data log.debug "Selected song = $state.selectedSong" } else if (song == state.selectedSong?.station) { log.debug "Selected existing entry '$song', which is no longer in the last 20 list" } else { log.warn "Selected song '$song' not found" } } catch (Throwable t) { log.error t } } private anythingSet() { for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes","timeOfDay"]) { if (settings[name]) { return true } } return false } private ifUnset(Map options, String name, String capability) { if (!settings[name]) { input(options, name, capability) } } private ifSet(Map options, String name, String capability) { if (settings[name]) { input(options, name, capability) } } def installed() { log.debug "Installed with settings: ${settings}" subscribeToEvents() } def updated() { log.debug "Updated with settings: ${settings}" unsubscribe() unschedule() subscribeToEvents() } def subscribeToEvents() { subscribe(app, appTouchHandler) subscribe(contact, "contact.open", eventHandler) subscribe(contactClosed, "contact.closed", eventHandler) subscribe(acceleration, "acceleration.active", eventHandler) subscribe(motion, "motion.active", eventHandler) subscribe(mySwitch, "switch.on", eventHandler) subscribe(mySwitchOff, "switch.off", eventHandler) subscribe(arrivalPresence, "presence.present", eventHandler) subscribe(departurePresence, "presence.not present", eventHandler) subscribe(smoke, "smoke.detected", eventHandler) subscribe(smoke, "smoke.tested", eventHandler) subscribe(smoke, "carbonMonoxide.detected", eventHandler) subscribe(water, "water.wet", eventHandler) subscribe(button1, "button.pushed", eventHandler) if (triggerModes) { subscribe(location, modeChangeHandler) } if (timeOfDay) { schedule(timeOfDay, scheduledTimeHandler) } if (song) { saveSelectedSong() } loadText() } def eventHandler(evt) { log.trace "eventHandler($evt?.name: $evt?.value)" if (allOk) { log.trace "allOk" def lastTime = state[frequencyKey(evt)] if (oncePerDayOk(lastTime)) { if (frequency) { if (lastTime == null || now() - lastTime >= frequency * 60000) { takeAction(evt) } else { log.debug "Not taking action because $frequency minutes have not elapsed since last action" } } else { takeAction(evt) } } else { log.debug "Not taking action because it was already taken today" } } } def modeChangeHandler(evt) { log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" if (evt.value in triggerModes) { eventHandler(evt) } } def scheduledTimeHandler() { eventHandler(null) } def appTouchHandler(evt) { takeAction(evt) } private takeAction(evt) { log.trace "takeAction()" if (song) { //sonos.playSoundAndTrack(state.sound.uri, state.sound.duration, state.selectedSong, volume) } else if (resumePlaying){ //sonos.playTrackAndResume(state.sound.uri, state.sound.duration, volume) } else { //sonos.playTrackAndRestore(state.sound.uri, state.sound.duration, volume) } if (frequency || oncePerDay) { state[frequencyKey(evt)] = now() } log.trace "Exiting takeAction()" } private frequencyKey(evt) { "lastActionTimeStamp" } private dayString(Date date) { def df = new java.text.SimpleDateFormat("yyyy-MM-dd") if (location.timeZone) { df.setTimeZone(location.timeZone) } else { df.setTimeZone(TimeZone.getTimeZone("America/New_York")) } df.format(date) } private oncePerDayOk(Long lastTime) { def result = true if (oncePerDay) { result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true log.trace "oncePerDayOk = $result" } result } // TODO - centralize somehow private getAllOk() { modeOk && daysOk && timeOk } private getModeOk() { def result = !modes || modes.contains(location.mode) log.trace "modeOk = $result" result } private getDaysOk() { def result = true if (days) { def df = new java.text.SimpleDateFormat("EEEE") if (location.timeZone) { df.setTimeZone(location.timeZone) } else { df.setTimeZone(TimeZone.getTimeZone("America/New_York")) } def day = df.format(new Date()) result = days.contains(day) } log.trace "daysOk = $result" result } private getTimeOk() { def result = true if (starting && ending) { def currTime = now() def start = timeToday(starting, location?.timeZone).time def stop = timeToday(ending, location?.timeZone).time result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start } log.trace "timeOk = $result" result } private hhmm(time, fmt = "h:mm a") { def t = timeToday(time, location.timeZone) def f = new java.text.SimpleDateFormat(fmt) f.setTimeZone(location.timeZone ?: timeZone(time)) f.format(t) } private getTimeLabel() { (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" } // TODO - End Centralize private loadText() { switch ( actionType) { case "Bell 1": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"] break; case "Bell 2": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell2.mp3", duration: "10"] break; case "Dogs Barking": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/dogs.mp3", duration: "10"] break; case "Fire Alarm": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/alarm.mp3", duration: "17"] break; case "The mail has arrived": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/the+mail+has+arrived.mp3", duration: "1"] break; case "A door opened": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/a+door+opened.mp3", duration: "1"] break; case "There is motion": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/there+is+motion.mp3", duration: "1"] break; case "Smartthings detected a flood": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+a+flood.mp3", duration: "2"] break; case "Smartthings detected smoke": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+smoke.mp3", duration: "1"] break; case "Someone is arriving": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/someone+is+arriving.mp3", duration: "1"] break; case "Piano": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/piano2.mp3", duration: "10"] break; case "Lightsaber": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"] break; case "Custom Message": if (message) { //state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed) } else { //state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App") } break; default: state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"] break; } }