Sonos Notify with Sound to multiple Sonos Devices

Of course you can modify the app you mention by setting multiple: true which will give you the ability to select multiple Sonos for the notification but haven’t used it. So cannot comment how well it works.

Unfortunately I do not have a personal laptop so can’t show you the code to modify…, :frowning: Two dead laptops and all funds gone towards devices… Hope the experts can chime in.

I have 5 Sonos devices integrated into ST: Two Play:5’s as a paired L/R set with a Sonos base in our living room, one Play:1 in the bathroom and a Play:5 in the Kitchen. I most always have them grouped using the native Sonos APP with the Kitchen Sonos being the lead Sonos in the music selection and grouping.

I configured the ST Sonos Notify APP to play a canned ST message when our mailbox with a contact switch is opened “You’ve Got Mail”. The Sonos system never plays when you expect it or rarely at all. Sometimes the Sonos Notify APP pauses the Sonos group that is playing, and plays nothing. When I check why, the “You have Mail.mp3” is queued but not playing. I have to play the message and then manually restart the music. Needless to say, I have de-installed the ST Sonos Notify APP. I do not use ST APP to control my Sonos systems since the native Sonos APP does that so well.

Many ST community forum members have been very disappointed in ST and the weak Sonos integration when it comes to having Sonos either play music or suspend music currently playing and provide a custom message and return playing the song that was playing before the Sonos Notify.

The ST Sonos Notify App might work for some people who do not group their Sonos speakers or have them playing when an event fires, but that is not how we use our Sonos system. Why have Sonos and not group or have them play all the time?

3 Likes

I made a quick change to the app you can use here. All I did was ad “multiple: True” to line 95.

/**
 *  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.
 *
 *  Sonos Custom Message
 *
 *  Author: SmartThings
 *  Date: 2014-1-29
 */
definition(
	name: "Sonos Notify with Sound_Multiple",
	namespace: "smartthings",
	author: "SmartThings",
	description: "Play a sound or custom message through your Sonos 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: "mainPage", title: "Play a message on your Sonos when 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 "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, multiple: 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;
		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;
	}
}
3 Likes

That worked perfectly! Thank you.

For those of you using Sonos, and likely in much more sophisticated ways, would you say that 1 Sonos to start, with ST integration, works as you would expect? Notifications and ideally with custom sounds, then playing normal music? Or how would you start with a simple approach? Thanks, I haven’t invested in one yet but considering.

I currently have one Sonos and several of the Cheap Sonos connected. I started with the real Sonos first and it did work well, I am very happy with it. We are frequently in our family room in the basement and I use it mainly for notifications for the arrival of family members and a doorbell (kind of) it notifies when the sensor on the screen door opens (good place to start). I have not explored the custom options yet but I have done some reading and it seems possible by adding some code and hosting the files somewhere you can access them from like google drive (I got side tracked with the cheap sonos about this time and thought it would be more beneficial).

I have one of the Cheap Sonos connected in our bedroom playing a relaxing music station from Radio Tunes with the wifi dlna music box (it had a different name at the time I purchased it, seller has changed the name, looks the same though) using the code here.

Additionally I am using an old cell phone connected to PC speakers running with the android app Private Dancer, which plays a convincing barking dog sound when visitors trigger the motion sensor on the front porch.

I have not really played music through the smartthings app outside of what I have mentioned, I do not think it gives you the ability to “browse” for music, I think it only lets you play/stop/rwd/ff music from what is already been playing, requiring it to be accessible all the time, like streaming or something on your network that is accessible all the time.

1 Like

Anyone know where the heck they moved the sonos notification on this new app layout?

1 Like

I wonder if the removed it. I can’t find it anywhere.

1 Like

I have the same issue, it simply does not work, no message coming out of my Sonos, I ask for weather forecast or good morning but neither are reliable and almost never work…

1 Like

Sorry my ignorance on this matter but how can I use your lines of code? Where to paste them? :grin:

you need to go to the developer portal and paste the code as a new smart app. check out the link below as a step by step guide.

http://docs.smartthings.com/en/latest/getting-started/first-smartapp.html

1 Like

cuboy29 - Thank you for the posting the code - it works great for me.
pjorio - You need a ST developer account to use this code to create your own SmartApp.

1 Like

I tried this new code and still came up with the same error code “failed to save page: mainpage” as I did with the original Notify with sound app. Any other ideas on how to fix this issue or did ST completely shut this down?

I modified the app just as you mentioned. There can be a variable delay between device playback of a few hundred ms. Annoying to ok, depending the clip being played.

I have used multiple true which does work. But cuts off at times. What I do not is to designate the three speakers to where I am based on home, master bedroom etc. and play the notification on that speaker via custom app. Pretty much always works.

The ones in family room is paired as left and right with zero delay and notification plays on both even though you select the primary in the ST app and ignore the other.

THe L speaker is the primary and the R is paired to it. When Sonos connect finds them, I ignore the R.

Master bedroom is independent and used in Night mode only to play notifications.

And now with Apple Music integration it rocks! :slight_smile:

Morning,

I wanted a small tweak to the app to enable the message to run after a 1 minute delay. I currently have my phone set as a presence sensor so when I enter my garage the custom message begins to play. However if I can trigger the message to begin 1 minute after ST detects my presence it should time correctly.

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) { **runIn(60**(runstate.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; } }

I have always been lousy at using the commands to delay an action but I inserted runIn(60(…)) prior to call to action to play the custom message.

Does this appear to be correct?

I have tried this approach and there are a few drawbacks it seems or maybe it’s just my misunderstanding. For every action you want (aka “wife has arrived”, “wife has left”, “contact sensor open”, etc, etc, you need to create a new SmartApp instance of the “Sonos Notify with Sound_Multiple” designated just for that action so depending on how many actions/triggers you want, you can have several instances of the SmartApp which doesn’t seem right. This is where the SmartApp called “Big Talker” excels in that it allows multiple actions/triggers to be created in itself instead of over-flooding your SmartApp section, however “Big Talker” did not work at all for me.

Also, after getting the first 2 instances setup, I now received the error above that @GG23 mentioned which now prevents me from creating and/or modifying new instances of the SmartApp for handling existing/new actions/triggers. After closer inspection from the “Live Logging” in the dev site, I noticed that with that error there is an “UndeclaredThrowableException” at line 413 from the code that @cuboy29 provided above and that line points to the notorious fn call that I believe is causing all other SmartApps to fail which is the ST textToSpeech fn so unfortunately, while it worked for the first 2 attempts, it now fails constantly over and over so oh well.

Why does this repeat the message 4 times? Anybody know how to have it play once?

Why the message is not playing on the sonos that I picked.

Hi,

did anyone figured out how to fix the code that can use the Temporarily change volume on an custom Message ? I have a group of Sonos players and with the code postet here, the custom message only will change the volume on one speaker (the first one in the group). I think the input of this code line must be fixed:

input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true, multiple: true

It has to be some code that tells ST to input a Sonos Group not a single music.Player, so the the Temporarily change volume will be done by the whole grouped sonos players.

Did anyone has an clue to get the problem solved ?

Thanks