Sonos Play Custom Message Question - delay playing?


(Brian Smith) #1

I would like a custom message to play when a certain presence sensor arrives home. However, I need to compensate for the delay of when it arrives and when they finally get into the house. I see a “minimum time between actions” listed as an option. Is that the delay from when the presence sensor triggers and when the SmartApp executes? (If so, it should be re-written to say “delay between trigger and running of smartapp” or something like that)

Or… Is it possible to have the following trigger:

  • Presence sensor arrives
  • garage door opens (it is on a sensor)
  • delay of a minute or two ensuring that the person carrying that sensor is in the house (it is our daughter’s backpack, so it could be my wife walking in first)

Cant figure out how to get Sonos to act on 2 independent events
Why long delay for custom spoken messages?
(Greg) #2

the “minimum time between actions” means the following

say you put 1 hour. if you arrive twice in one hour the sonos will only report the first one. if you arrive again at 1h 1m it when then play your message and start the timer again

I would like your delay option in my apps too.


(John Esher) #3

I wanted to do the same thing. I just chose another sensor (door open/closed) that I figured my son was most likely to use. So theoretically when he returned home and opened the door the Happy Birthday message would play.

But my in-laws got here first and left the door open. So as soon as my son “returned” (which was 500’ away in reality) the message played.

They told him about, but it wasn’t the same. Have to wait 'til next year.


Text-to-Speech is back!
(Brian Smith) #4

This is where some “if this and that” could help. Hopefully we will start to see that make its way into the system.


(Chuckles) #5

Here you go…

I’ve created a modified version of “Sonos Notify with Sound”. My version is called "Sonos Delayed Notify with Sound" and takes an extra configuration parameter - a time delay in minutes.

You’ll need to use the IDE to install it - I’ve shared the code so you can use the normal procedure for installing a shared app.


(John Rubin) #6

Hello @chuckles , I am brand new to SmartThings, still trying to get a handle of what exactly SmartApps are. I have the same question/request as the OP.

How exactly would I be able to view the code you wrote for the SmartApp? You say you’ve shared the code…where would one find it? I am registered to develop for SmartThings and have access to the IDE. I apologize for my ignorance, I am only just getting into the platform.

Thank you!


(Chuckles) #7

Hey @jrubin,

Though I’ve been reading the forums since the Kickstarter days, I only received my hardware last week, so I’m new to the SmartApp coding scene too.

Here’s how to get a copy of my modified version:

In the IDE:

  1. Go to My SmartApps
  2. Click the “+ New SmartApp” button
  3. Give your app a name (any name will do - it will be overwritten
    later)
  4. Give your app an author (again, doesn’t matter who)
  5. Give your app a description (again, doesn’t matter what)
  6. Scroll all the way down and click the “Create” button
  7. The IDE will create a new app with boilerplate code based on your
    earlier inputs
  8. Click “Browse SmartApps” (over near the top right).
  9. From the dropdown, click “Browse Shared Smartapps
  10. Now you need to find my app - “Sonos Delayed Notify with
    Sound”. Right now it’s easy to find because it’s right near the top
    of the list amongst the most recent.
  11. When you’ve found my app and can see the code, click the
    "Overwrite" button (down near the bottom right).
  12. The IDE will return to your app, but it has blown away all your
    earlier boilerplate code and replaced it with my code.
  13. Click “Save” - the IDE should tell you it saved successfully.
  14. Click “Publish” and then, from the dropdown, click “For Me
    • the IDE should tell you the app has successfully been published “for me”.

Your app is now ready for you to install an instance via your phone.

  1. Open the SmartThings app on your phone and go to the Dashboard
  2. Scroll all the way down and click the “+” button
  3. Near the top, scroll across. Instead of going to “More” and then
    "SmartThings Labs", where the regular version of the Sonos apps
    reside, you’re instead going to keep going across to “My Apps
  4. You should now see your new "Sonos Delayed Notify with Sound"
    app. Select it and install as per normal. When you’re setting it up
    you’ll find the new addtional parameter to set a delay in minutes.

(John Rubin) #8

Hey @chuckles,
Thank you very much for the detailed steps, I thoroughly appreciate it. I will check this out.

Thanks again!


(Dawn Fairbro) #9

@chuckles,

Thank you. I too need a delay as it takes a moment to get the items out of the car and into the house. So far the only one who has heard my “welcome Home” is the cats on the other side of the door. I followed your instruction to the letter, super easy. I can’t program a DVR but I made a Smart App today. Thank you so much. Can’t wait to get home and see how it works 


(Chuckles) #10

Hi @dfairbro1

Glad my notes were useful. Let us know how it worked out (and pat the cats for me) :slight_smile:


(Dawn Fairbro) #11

@chuckles
it worked. For the first time I actually got to hear it. Thank you again


#12

Hi Chuckles,

Im looking for a code that I can delay 5 seconds after file is played:

else if (opt == “6”) {
sb << delim
sb << "NBA News;, "
if (isMetric) {
sb << rss3.channel.item[0].title
RUN 5 seconds delay

I looked through your code and couldn’t find an answers. Can you point me in the right direction?

Thanks


(Damon Floyd) #13

I know this topic is a little old, but I was messing with the same issue the other day. My problem is that the presence sensor (our phones) were being picked up long before we even got to the house, so there is a non-deterministic delay between being “home” and actually being in the house. I want a welcome message played on arrival, but if I base it on the presence sensor, it would play while we are down the street. This is how I solved it (assuming I am understanding your issue correctly).

What I did is create a new mode “Coming Home” and a new action “In Range.” Since you can restrict the automatic transition between modes based on what mode you are coming from, I set it to transition from “Away” -> “Coming Home” whenever the presence sensor was detected, then from “Coming Home” -> “Home” when any of the door were opened. If at any time the presence sensor leaves range before the door is opened, it just transitions back to “Away.” I only get push notifications on the transition to “Home” so that I don’t get spammed with the extra transitions. So far it works great.

The only issue I could foresee is if there was a lag in the presence sensor and the door was opened first it would only make it to the “Coming Home” mode until another door was opened. You could add other criteria to get around this (e.g. motion is detected).


(João Cautela) #14

Is it possible to use this app again?

Can´t find it on the template.


(Chuckles) #15

The IDE shared apps area has changed, presumably as part of the whole app publication process which has been slowly evolving.

Here is the code in question:

/**
 *  Sonos Custom Message
 *
 *    Author: SmartThings
 *      Date: 2014-1-29
 *
 *  Modified: Chuckles
 *            Added configurable delay before playing sound
 *      Date: 2014-10-25
 */
definition(
    name: "Sonos Delayed Notify with Sound",
    namespace: "chuckles",
    author: "SmartThings",
    description: "Play a sound or custom message through your Sonos when the mode changes or other events occur and a specified delay period has elapsed.",
    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: "mainPage", title: "Play a message on your Sonos following a delay after something happens", install: true, uninstall: true)
    page(name: "chooseTrack", title: "Select a song or station")
    page(name: "timeIntervalInput", title: "Only during a certain time") {
        section {
            input "starting", "time", title: "Starting", required: false
            input "ending", "time", title: "Ending", 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 "delayTime", "number", title: "Delay (mins)?", required: true, defaultValue: "0", multiple: false
        }
        section{
            input "actionType", "enum", title: "Action?", required: true, defaultValue: "Custom Message", 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 Sonos 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) {
        runDaily(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) {
                    scheduleAction(evt)
                }
                else {
                    log.debug "Not taking action because $frequency minutes have not elapsed since last action"
                }
            }
            else {
                scheduleAction(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) {
    scheduleAction(evt)
}

private scheduleAction(evt) {

    log.trace "scheduleAction()"
   
    if (delayTime.toInteger()) {
        runIn((delayTime.toInteger() * 60), "takeAction")
    } else {
        // delayTime zero - execute immediately
        takeAction()
    }
   
    if (frequency || oncePerDay) {
        state[frequencyKey(evt)] = now()
    }

   log.trace "Exiting scheduleAction()"
}

private takeAction() {

    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)
    }

    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).time
        def stop = timeToday(ending).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;
        default:
            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;
    }
}

Text-to-Speech is back!