I love walking around the house and having music playing. However, most of the time when I’m home alone, or if the internal doors are closed, I don’t need the music to be in every room. When playing music in other rooms, it would be nice for the speakers to group and un-group only when they’re needed.

Ingredients

Speakers. The most import piece of the puzzle. I’m using different types of Sonos Speakers, including the Sonos Play:1, the new Sonos One speaker, and a Sonos Playbar. Home Assistant has a great Sonos component, which exposes methods for grouping players together.

Software. I’m using Home Assistant as my Home Automation controller. You might be able to substitute this for another controller of your choice though.

Sensors. I’m using Z-wave motion sensors, specifically the Aeotec 4-in-1 Multisensors and Aeotec 6-in-1 Multisensors, to detect motion around my home. These have a simple “on” or “off” state in Home Assistant depending whether motion has been detected or not. You could also get a bit fancy and use door/window sensors or any other type of sensor available to you in Home Assistant.

It seems like a pretty basic idea; when motion is detected in Home Assistant, join the Sonos speaker in the room to the group. When motion is not detected, remove the speaker from the group. However, we need to give Home Assistant some more information so it knows what it needs to do.

The Latest In Your Inbox Enter your email address below to receive my latest blog posts and videos about Home Automation in your Inbox

Adding a Toggle Switch

We first want a main switch, which can be enabled or disabled. When this switch is turned on, Home Assistant will automatically group Sonos players together when motion is detected. If the switch is turned off, Home Assistant won’t do anything. This is important, as we might not always want music to follow us around the home.

We can easily do this with an input_boolean . Let’s set one up now.

configuration.yaml input_boolean: follow_music: name: Follow Music initial: 'off' 1 2 3 4 5 6 input _ boolean : follow _ music : name : Follow Music initial : 'off'

Defining a Master Player

Home Assistant needs to know which player is the main/master player. This is important. Let’s assume you’re playing music in your kitchen and then you go into the bedroom. Home Assistant groups the kitchen and bedroom speakers together. Eventually motion will no longer be detected in the kitchen, so now the kitchen player is un-grouped. The only Sonos speaker playing music is the bedroom. This would be okay, but let’s assume you’re reading a book in your chair. Motion will stop being detected, and Home Assistant will go ahead and un-group the bedroom player as well. This will cause the music to stop playing in the bedroom (and now the entire house) altogether. No amount of flapping your hands will turn the music back on. What’s worse is what you were listening to will most likely be forgotten, because your speaker was un-grouped instead of pausing.

The purpose of the master player is to give Home Assistant a point of reference. It is the one player in the house which Home Assistant will not pause, or un-group. It will keep playing music until someone tells it to stop.

The master player could be a different speaker at any time. If you’re working in the office, your master player may be your office Sonos Play:3. If you’re cooking in the kitchen, then the master player may be the kitchen Sonos One. Because of this, we want a way for us to be able to easily select which speaker is the “master” speaker. I’m going to use an input_select component to do that.

configuration.yaml input_select: music_controller: name: Music Controller options: - office - kitchen - main_bedroom 1 2 3 4 5 6 7 8 9 input _ select : music _ controller : name : Music Controller options : - office - kitchen - main _ bedroom

You’ll notice that the names of those speakers seem a bit rough. I’ve deliberately named them kitchen as opposed to Kitchen, or main_bedroom instead of Main Bedroom. This is because I am basing them on their Home Assistant entity_id . For example, in my Home Assistant I have media_player.kitchen and media_player.main_bedroom which are Sonos speakers. You’ll see why this is important soon.

Group Players Script

The first thing I’m going to do is create two Home Assistant scripts. One to group players, and another to un-group players. The use of scripts is important. If we ever get new Sonos speakers in the future, we only need to update the automation file for the logic. The scripts should be just set-and-forget.

First, here is a script that will add a new player to a Sonos Group.

scripts.yaml alias: "Sonos Group Motion" sequence: - condition: template value_template: > {% if target_player is not none and target_player != false and target_player != '' %} true {% else %} false {% endif %} # The target player must not be playing anything - condition: template value_template: > {% if states(target_player) != 'playing' %} true {% else %} false {% endif %} #Now join the player into the group - service: sonos.join data_template: master: media_player.{{ states('input_select.music_controller') }} entity_id: > {% if target_player is not none %} {{ target_player }} {% endif %} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 alias : "Sonos Group Motion" sequence : - condition : template value _ template : > { % if target _ player is not none and target _ player ! = false and target _ player ! = '' % } true { % else % } false { % endif % } # The target player must not be playing anything - condition : template value _ template : > { % if states ( target _ player ) ! = 'playing' % } true { % else % } false { % endif % } #Now join the player into the group - service : sonos.join data _ template : master : media_player. { { states ( 'input_select.music_controller' ) } } entity _ id : > { % if target _ player is not none % } { { target _ player } } { % endif % }

That script does the following:

It accepts a parameter called target_player . This is the entity_id of the player you want to add to the group. So, if we move into the kitchen and want the kitchen Sonos to start playing, it would be set to media_player . kitchen . It checks to make sure the target player is not already playing something. This is useful if you’re watching TV on a Sonos Playbar, or you’re listening to music in different rooms. If the player is already playing something, don’t do anything. If the player isn’t playing anything, it will then group the target player to the player which is selected with the input_select . music_controller drop-down in the UI.

Before we go ahead and save that, I want to add one more feature. You’ll thank me for this.

Let’s assume you’ve watched TV the night before, and your Playbar is set to 45%. You’re listening to music at a peaceful 25%, and walk into the living room. Our script above includes in your Playbar, and because the Playbar is set to 45% volume, when it gets grouped it will start playing music at 45% and scare the absolute **** out of you. You’ll be fumbling for your phone or diving to the Playbar to try and frantically lower the volume before you wake-up someone in the house.

To solve this, I’m going to alter the script above to first set the volume of the target player to the same level as the controller. This means no matter what the previous volume level was, we will always set it back to the same level as the controller player first, to avoid that big shock.

scripts.yaml alias: "Sonos Group Motion" sequence: - condition: template value_template: > {% if target_player is not none and target_player != false and target_player != '' %} true {% else %} false {% endif %} # The target player must not be playing anything - condition: template value_template: > {% if states(target_player) != 'playing' %} true {% else %} false {% endif %} # First set the target player to the same volume as the controller - service: media_player.volume_set data_template: entity_id: > {% if target_player is not none %} {{ target_player }} {% endif %} volume_level: > {% for state in states.media_player if state.entity_id == 'media_player.' + states('input_select.music_controller') %} {{state.attributes.volume_level }} {% endfor %} #Now join the player into the group - service: sonos.join data_template: master: media_player.{{ states('input_select.music_controller') }} entity_id: > {% if target_player is not none %} {{ target_player }} {% endif %} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 alias : "Sonos Group Motion" sequence : - condition : template value _ template : > { % if target _ player is not none and target _ player ! = false and target _ player ! = '' % } true { % else % } false { % endif % } # The target player must not be playing anything - condition : template value _ template : > { % if states ( target _ player ) ! = 'playing' % } true { % else % } false { % endif % } # First set the target player to the same volume as the controller - service : media_player.volume_set data _ template : entity _ id : > { % if target _ player is not none % } { { target _ player } } { % endif % } volume _ level : > { % for state in states . media _ player if state . entity _ id == 'media_player.' + states ( 'input_select.music_controller' ) % } { { state . attributes . volume _ level } } { % endfor % } #Now join the player into the group - service : sonos.join data _ template : master : media_player. { { states ( 'input_select.music_controller' ) } } entity _ id : > { % if target _ player is not none % } { { target _ player } } { % endif % }

Un-group Players Script

Now that we have a script to group players together, we need one which will do the opposite. This will be called when we leave a room.

scripts.yaml alias: "Sonos Ungroup Motion" sequence: - condition: template value_template: > {% if target_player is not none and target_player != false and target_player != '' %} true {% else %} false {% endif %} # The target player must be playing - condition: template value_template: > {% if states(target_player) == 'playing' %} true {% else %} false {% endif %} # The target must not be the co-ordinator - condition: template value_template: > {% if target_player != 'media_player.' + states('input_select.music_controller') %} true {% else %} false {% endif %} - service: sonos.unjoin data_template: entity_id: > {% if target_player is not none %} {{ target_player }} {% endif %} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 alias : "Sonos Ungroup Motion" sequence : - condition : template value _ template : > { % if target _ player is not none and target _ player ! = false and target _ player ! = '' % } true { % else % } false { % endif % } # The target player must be playing - condition : template value _ template : > { % if states ( target _ player ) == 'playing' % } true { % else % } false { % endif % } # The target must not be the co-ordinator - condition : template value _ template : > { % if target _ player ! = 'media_player.' + states ( 'input_select.music_controller' ) % } true { % else % } false { % endif % } - service : sonos.unjoin data _ template : entity _ id : > { % if target _ player is not none % } { { target _ player } } { % endif % }

This script just calls sonos.unjoin against our player, but only if the player is playing.

Adding the Automations

Now we’ve got the scripts setup, it’s time for the fun part. The automations. I try to keep my automations as lean as possible. You could create an automation for each sensor, naming them like “group study player when motion detected”. However I prefer to keep everything in a single automation if I can. So, here is my automation for grouping players when motion is detected.

automation.yaml - alias: Group Player when motion detected trigger: - platform: state entity_id: binary_sensor.entry_motion_227 #Entry to: 'on' - platform: state entity_id: binary_sensor.bedroom_motion_106 to: 'on' condition: condition: and conditions: - condition: state entity_id: input_boolean.follow_music state: 'on' # The controller player must be playing music (not paused) - condition: template value_template: > {% if states('media_player.' + states('input_select.music_controller')) == 'playing' %} true {% else %} false {% endif %} action: - service: script.sonos_group_from_motion data_template: target_player: > {% if trigger.entity_id == 'binary_sensor.entry_motion_227' %} {% set player = 'media_player.kitchen' %} {% elif trigger.entity_id == 'binary_sensor.bedroom_motion_106' %} {% set player = 'media_player.main_bedroom' %} {% endif %} {% if player is not none %} {{ player }} {% else %} false {% endif %} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 - alias : Group Player when motion detected trigger : - platform : state entity _ id : binary_sensor.entry_motion_227 #Entry to : 'on' - platform : state entity _ id : binary_sensor.bedroom_motion_106 to : 'on' condition : condition : and conditions : - condition : state entity _ id : input_boolean.follow_music state : 'on' # The controller player must be playing music (not paused) - condition : template value _ template : > { % if states ( 'media_player.' + states ( 'input_select.music_controller' ) ) == 'playing' % } true { % else % } false { % endif % } action : - service : script.sonos_group_from_motion data _ template : target _ player : > { % if trigger . entity _ id == 'binary_sensor.entry_motion_227' % } { % set player = 'media_player.kitchen' % } { % elif trigger . entity _ id == 'binary_sensor.bedroom_motion_106' % } { % set player = 'media_player.main_bedroom' % } { % endif % } { % if player is not none % } { { player } } { % else % } false { % endif % }

There’s only two conditions for this script to fire. First, our input_boolean for following music must be turned on. Second, the controller player must be playing music. If no music is playing on the controller, then there’s nothing to do.

If those conditions are met, then our script script.sonos_group_from_motion is called. (Be sure to check how your script is named, my scripts are saved in individual files which determine their entity_id’s. See here for more information.)

The magic comes in the template used to determine the target_player . We’re using the trigger variable, which is passed to to the automation. It will allow us to check which motion sensor triggered the automation to fire. We can then match the motion sensor up to the player which is in the room. This comes in handy if we ever need to add additional speakers and motion sensors in the mix. We just need to update these automation templates.

There’s also a similar automation to un-group the players when motion is not detected:

automation.yaml - alias: Ungroup Player when motion not detected trigger: - platform: state entity_id: binary_sensor.entry_motion_227 #Entry to: 'off' - platform: state entity_id: binary_sensor.bedroom_motion_106 to: 'off' condition: condition: and conditions: - condition: state entity_id: input_boolean.follow_music state: 'on' action: - service: script.sonos_ungroup_from_motion data_template: target_player: > {% if trigger.entity_id == 'binary_sensor.entry_motion_227' %} {% set player = 'media_player.kitchen' %} {% elif trigger.entity_id == 'binary_sensor.bedroom_motion_106' %} {% set player = 'media_player.main_bedroom' %} {% endif %} {% if player is not none %} {{ player }} {% else %} false {% endif %} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 - alias : Ungroup Player when motion not detected trigger : - platform : state entity _ id : binary_sensor.entry_motion_227 #Entry to : 'off' - platform : state entity _ id : binary_sensor.bedroom_motion_106 to : 'off' condition : condition : and conditions : - condition : state entity _ id : input_boolean.follow_music state : 'on' action : - service : script.sonos_ungroup_from_motion data _ template : target _ player : > { % if trigger . entity _ id == 'binary_sensor.entry_motion_227' % } { % set player = 'media_player.kitchen' % } { % elif trigger . entity _ id == 'binary_sensor.bedroom_motion_106' % } { % set player = 'media_player.main_bedroom' % } { % endif % } { % if player is not none % } { { player } } { % else % } false { % endif % }

Using Multiple Motion Sensors for One Speaker

To improve accuracy, you may have setup multiple motion sensors in a room. This is particularly the case if you live in an open-plan apartment. We can change the template and add some conditions to account for these so that

If a room has multiple motion sensors, if any motion sensor activates the room should be grouped into the playing music; and

Only un-group said room if both motion sensors have not detected motion

I’m going to set this up on my kitchen speaker. I have two motion sensors that can detect motion near the kitchen, one is the entry motion sensor (which is already in the automation). I’m going to add in the dining motion sensor, with the rules above.

automation.yaml - alias: Group Player when motion detected trigger: - platform: state entity_id: binary_sensor.entry_motion_227 #Entry to: 'on' - platform: state entity_id: binary_sensor.dining_motion_102 #Dining to: 'on' - platform: state entity_id: binary_sensor.bedroom_motion_106 to: 'on' condition: condition: and conditions: - condition: state entity_id: input_boolean.follow_music state: 'on' # The controller player must be playing music (not paused) - condition: template value_template: > {% if states('media_player.' + states('input_select.music_controller')) == 'playing' %} true {% else %} false {% endif %} action: - service: script.sonos_group_from_motion data_template: target_player: > {% if trigger.entity_id == 'binary_sensor.entry_motion_227' or trigger.entity_id == 'binary_sensor.dining_motion_102' %} {% set player = 'media_player.kitchen' %} {% elif trigger.entity_id == 'binary_sensor.bedroom_motion_106' %} {% set player = 'media_player.main_bedroom' %} {% endif %} {% if player is not none %} {{ player }} {% else %} false {% endif %} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 - alias : Group Player when motion detected trigger : - platform : state entity _ id : binary_sensor.entry_motion_227 #Entry to : 'on' - platform : state entity _ id : binary_sensor.dining_motion_102 #Dining to : 'on' - platform : state entity _ id : binary_sensor.bedroom_motion_106 to : 'on' condition : condition : and conditions : - condition : state entity _ id : input_boolean.follow_music state : 'on' # The controller player must be playing music (not paused) - condition : template value _ template : > { % if states ( 'media_player.' + states ( 'input_select.music_controller' ) ) == 'playing' % } true { % else % } false { % endif % } action : - service : script.sonos_group_from_motion data _ template : target _ player : > { % if trigger . entity _ id == 'binary_sensor.entry_motion_227' or trigger . entity _ id == 'binary_sensor.dining_motion_102' % } { % set player = 'media_player.kitchen' % } { % elif trigger . entity _ id == 'binary_sensor.bedroom_motion_106' % } { % set player = 'media_player.main_bedroom' % } { % endif % } { % if player is not none % } { { player } } { % else % } false { % endif % }

And now to make sure both motion detectors have not seen motion before un-grouping:

automation.yaml - alias: Ungroup Player when motion not detected trigger: - platform: state entity_id: binary_sensor.entry_motion_227 #Entry to: 'off' - platform: state entity_id: binary_sensor.dining_motion_102 #Dining to: 'off' - platform: state entity_id: binary_sensor.bedroom_motion_106 to: 'off' condition: condition: and conditions: - condition: state entity_id: input_boolean.follow_music state: 'on' # If this is the entry or dining, make sure both are off before ungrouping - condition: template value_template: > {% if trigger.entity_id != 'binary_sensor.entry_motion_227' and trigger.entity_id != 'binary_sensor.dining_motion_102' %} true {% else %} {% if states('binary_sensor.entry_motion_227') == 'off' and states('binary_sensor.dining_motion_102') == 'off' %} true {% else %} false {% endif %} {% endif %} action: - service: script.sonos_ungroup_from_motion data_template: target_player: > {% if trigger.entity_id == 'binary_sensor.entry_motion_227' or trigger.entity_id == 'binary_sensor.dining_motion_102' %} {% set player = 'media_player.kitchen' %} {% elif trigger.entity_id == 'binary_sensor.bedroom_motion_106' %} {% set player = 'media_player.main_bedroom' %} {% endif %} {% if player is not none %} {{ player }} {% else %} false {% endif %} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 - alias : Ungroup Player when motion not detected trigger : - platform : state entity _ id : binary_sensor.entry_motion_227 #Entry to : 'off' - platform : state entity _ id : binary_sensor.dining_motion_102 #Dining to : 'off' - platform : state entity _ id : binary_sensor.bedroom_motion_106 to : 'off' condition : condition : and conditions : - condition : state entity _ id : input_boolean.follow_music state : 'on' # If this is the entry or dining, make sure both are off before ungrouping - condition : template value _ template : > { % if trigger . entity _ id ! = 'binary_sensor.entry_motion_227' and trigger . entity _ id ! = 'binary_sensor.dining_motion_102' % } true { % else % } { % if states ( 'binary_sensor.entry_motion_227' ) == 'off' and states ( 'binary_sensor.dining_motion_102' ) == 'off' % } true { % else % } false { % endif % } { % endif % } action : - service : script.sonos_ungroup_from_motion data _ template : target _ player : > { % if trigger . entity _ id == 'binary_sensor.entry_motion_227' or trigger . entity _ id == 'binary_sensor.dining_motion_102' % } { % set player = 'media_player.kitchen' % } { % elif trigger . entity _ id == 'binary_sensor.bedroom_motion_106' % } { % set player = 'media_player.main_bedroom' % } { % endif % } { % if player is not none % } { { player } } { % else % } false { % endif % }

Automating When Music Follows You

We can choose when music should start to follow you around. I’ve setup an automation which automatically disables the input_boolean.follow_music once music has stopped playing for at least fifteen minutes. Here’s how I do that:

automation.yaml - alias: Disable follow music if controller has been paused trigger: # Check every 5 minutes - platform: time minutes: '/5' seconds: 0 condition: condition: and conditions: - condition: state entity_id: input_boolean.follow_music state: 'on' # The controller player must NOT be playing - condition: template value_template: > {% if states('media_player.' + states('input_select.music_controller')) != 'playing' %} true {% else %} false {% endif %} # The controller player must be paused for at least 15 minutes (900 seconds) - condition: template value_template: > {% for state in states.media_player if state.entity_id == 'media_player.' + states('input_select.music_controller') %} {% if as_timestamp(now()) | int - as_timestamp(state.last_changed) > 900 %} true {% else %} false {% endif %} {% else %} false {% endfor %} action: - service: homeassistant.turn_off entity_id: input_boolean.follow_music 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 - alias : Disable follow music if controller has been paused trigger : # Check every 5 minutes - platform : time minutes : '/5' seconds : 0 condition : condition : and conditions : - condition : state entity _ id : input_boolean.follow_music state : 'on' # The controller player must NOT be playing - condition : template value _ template : > { % if states ( 'media_player.' + states ( 'input_select.music_controller' ) ) ! = 'playing' % } true { % else % } false { % endif % } # The controller player must be paused for at least 15 minutes (900 seconds) - condition : template value _ template : > { % for state in states . media _ player if state . entity _ id == 'media_player.' + states ( 'input_select.music_controller' ) % } { % if as _ timestamp ( now ( ) ) | int - as _ timestamp ( state . last _ changed ) > 900 % } true { % else % } false { % endif % } { % else % } false { % endfor % } action : - service : homeassistant.turn_off entity _ id : input_boolean.follow_music

That automation will check the controller speaker every five minutes. If it has not been playing anything for more than fifteen minutes, it turns off the input_boolean.follow_music input.

Another automation I use is to change the master/controller player to the office Sonos when my study desk turns on. I have a Z-wave Smart Switch which monitors the power usage of my PC monitor. When that goes up, it turns on an input_boolean which tells Home Assistant I am sitting at my desk, and not to turn the lights off if I’m sitting still.

automation.yaml - alias: If Study Desk in use, make it the controller trigger: - platform: state entity_id: input_boolean.study_desk to: 'on' for: minutes: 5 condition: condition: and conditions: - condition: state entity_id: input_boolean.follow_music state: 'on' - condition: state entity_id: input_boolean.study_desk state: 'on' # The controller must not already be set to the office - condition: template value_template: > {% if states('input_select.music_controller') != 'office' %} true {% else %} false {% endif %} action: - service: input_select.select_option entity_id: input_select.music_controller option: 'office' 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 - alias : If Study Desk in use , make it the controller trigger : - platform : state entity _ id : input_boolean.study_desk to : 'on' for : minutes : 5 condition : condition : and conditions : - condition : state entity _ id : input_boolean.follow_music state : 'on' - condition : state entity _ id : input_boolean.study_desk state : 'on' # The controller must not already be set to the office - condition : template value _ template : > { % if states ( 'input_select.music_controller' ) ! = 'office' % } true { % else % } false { % endif % } action : - service : input_select.select_option entity _ id : input_select.music_controller option : 'office'

I don’t have Sonos, can I use this with something else?

While I only use Sonos speakers for my multi-room audio, there are other platforms now that are starting to roll out multi-room audio as well, like Chromecast and Google Home. I’ve read on Reddit that some people are suggesting having motion sensors set the volume of other players to zero when there’s no motion. So, you could have a Chromecast Audio in your study, and one in your kitchen and when the kitchen motion sensor stops detecting motion, Home Assistant could set the volume on the kitchen Chromecast Audio to zero. When motion is detected again, it brings up the volume again. Another option is using Snapcast and Raspberry Pi’s. If these are something you’re interested in, here’s some links which may point you in the right direction:

Wrapping Up

This is a pretty neat way to use Home Assistant and templating to control things around the house. If you’re a sports fan, and have people over to watch the Cricket or Superbowl (I won’t judge you), you could setup a Sonos speaker in the bathroom. If someone goes into the bathroom, group the Sonos Playbar together with the bathroom speaker so people can hear every score while they’re in the bathroom. Or perhaps have a spooky Halloween track playing, and as people move around your house speakers tune in to the track to make it feel like those spooky voices are following them.