I have 4 Sonos speakers and occasionally found out that Phil’s blog on Making music follow you around the home with Home Assistant and Sonos. Followed his guide, I created my own version that has a little bit more control.
This is an introduction on this project from Phil:
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.
Please read Phil’s blog before continuing as I don’t want to just copy/paraphrase his words here.
In addition to the feature Phil implemented, I have enhanced his code to have more controls
- group with speaker when enterring a new room
- ungroup the speaker and pause the music when leaving the room for a while
- maintainance on master speaker
- pause the music if people leaves all the room that have sonos speakers
- do not group the speaker if the speaker is a soundbar and it is playing TV sound
I have a lot of motion sensors (Xiaomi Zigbee version) in my house which are used for auto-lighting, auto-heating. But they become useful for this project as well.
My Sonos devices:
Kitchen - Sonos Play 3
Master Room - Sonos Play 1
Living Room - Sonos Playbar
First Floor Corridor - Sonos Play 1
Originally I bought Sonos speaker with a Playbar and a pair of Play:1 as cinematic surround sound set-up. But I found Playbar is adequent for movies most of time, so I moved these two Play:1s into different room after I completed this project.
With HA Airsonos plug-in, I can also play music and even some video contents via Airplay so it is much flexiable than I originally thought.
My sonos speakers - master speaker variable:
input_select:
music_controller:
name: Sonos Music Master Speaker
options:
- master_room_sonos
- kitchen_sonos
- living_room_sonos
- first_corridor_sonos
Scripts:
###########################################
# Sonos Scrips
###########################################
add_sonos_into_speaker_group:
mode: queued
alias: Add Sonos Speaker Into the Speaker Group
fields:
target_player:
description: "Sonos player name that need to be added into the group"
example: "media_player.master_room_sonos"
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
# Play:3 sounds level needs to be offset for setting up Play:1/Playbars
- 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') %}
{% if states('input_select.music_controller') != 'kitchen_sonos' and target_player == 'media_player.kitchen_sonos' %}
{{ state.attributes.volume_level + 0.1 }}
{% elif states('input_select.music_controller') == 'kitchen_sonos' and target_player != 'media_player.kitchen_sonos' %}
{{ state.attributes.volume_level - 0.1 }}
{% else %}
{{ state.attributes.volume_level }}
{% endif %}
{% endfor %}
# Now join the player into the group twice in case sometimes it didn't manage to join in for certain cases
- service: sonos.join
data_template:
master: media_player.{{ states('input_select.music_controller') }}
entity_id: >
{% if target_player is not none %}
{{ target_player }}
{% else %}
media_player.living_room_sonos
{% endif %}
- service: sonos.join
data_template:
master: media_player.{{ states('input_select.music_controller') }}
entity_id: >
{% if target_player is not none %}
{{ target_player }}
{% else %}
media_player.living_room_sonos
{% endif %}
remove_sonos_from_speaker_group:
alias: Remove Sonos Speaker From the Speaker Group and Update the Master Speaker
mode: queued
fields:
target_player:
description: "Sonos player that need to be removed from the group"
example: "media_player.master_room_sonos"
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 is not the soundbar that is playing TV sound
- condition: template
value_template: >
{% if target_player is not none and state_attr(target_player, 'media_title') != 'TV' %}
true
{% else %}
false
{% endif %}
# Update the master speaker in the group
- service: input_select.select_option
entity_id: input_select.music_controller
data:
option: >
{% set ns = namespace() %}
{% set ns.primary_speaker = 'none' %}
{% set ns.secondary_speaker = 'none' %}
{# set the pri_speaker and sec_speaker #}
{% for speaker in state_attr(target_player, "sonos_group") %}
{% if loop.index == 1 %}
{% set ns.primary_speaker = speaker|regex_replace(find='media_player.', replace='', ignorecase=False) %}
{% elif loop.index == 2 %}
{% set ns.secondary_speaker = speaker|regex_replace(find='media_player.', replace='', ignorecase=False) %}
{% endif %}
{% endfor %}
{# use the second speaker as master speaker if target speaker is currently the master #}
{% if target_player == ('media_player.' + ns.primary_speaker) and ns.secondary_speaker != 'none' %}
{{ ns.secondary_speaker }}
{% else %}
{{ ns.primary_speaker }}
{% endif %}
# The target must be the slave to be removed from the group
- condition: template
value_template: >
{% if target_player != 'media_player.' + states('input_select.music_controller') %}
true
{% else %}
false
{% endif %}
- service: sonos.unjoin
data:
entity_id: >
{% if target_player is not none and target_player != false and target_player != '' and target_player != 'None'%}
{{ target_player }}
{% else %}
media_player.living_room_sonos
{% endif %}
pause_sonos_if_sole_speaker_group:
alias: Pause the Sonos Speaker if it is a Sole Speaker Group
mode: queued
fields:
target_player:
description: "Sonos player that need to be paused"
example: "media_player.master_room_sonos"
sequence:
- condition: template
value_template: >
{% if target_player is not none and target_player != false and target_player != '' %}
true
{% else %}
false
{% endif %}
# check it is a sole speaker
- condition: template
value_template: >
{% for speaker in state_attr(target_player, "sonos_group") %}
{% if loop.index == 1 %}
{% if loop.length == 1 %}
true
{% else %}
false
{% endif %}
{% endif %}
{% endfor %}
- service: media_player.media_pause
data:
entity_id: >
{% if target_player is not none and target_player != false and target_player != '' and target_player != 'None'%}
{{ target_player }}
{% else %}
media_player.living_room_sonos
{% endif %}
Grouping/Ungrouping Automations:
#################################################################
#
# Sonos Speakers Grouping/Ungrouping Based On Motion Sensor
#
#################################################################
- alias: S-LR Living Room Group Its Speaker If People Present
trigger:
- platform: state
from: "off"
to: "on"
entity_id:
- binary_sensor.living_room_sofa_motion_sensor_motion
- binary_sensor.living_room_tv_motion_sensor_motion
condition:
- 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:
# Add this room speaker into the group
- service: script.add_sonos_into_speaker_group
data:
target_player: media_player.living_room_sonos
- alias: S-LR Living Room Ungroup/Pause Its Speaker If No People
trigger:
- platform: state
from: "on"
to: "off"
for: 00:10:00
entity_id:
- binary_sensor.living_room_sofa_motion_sensor_motion
- binary_sensor.living_room_tv_motion_sensor_motion
- minutes: /5
platform: time_pattern
condition:
- condition: state
entity_id:
- binary_sensor.living_room_sofa_motion_sensor_motion
- binary_sensor.living_room_tv_motion_sensor_motion
for: 00:10:00
state: "off"
action:
- choose:
# IF - music follower is on
- conditions:
- condition: state
entity_id: input_boolean.follow_music
state: "on"
sequence:
# Remove this room speaker from the group
- service: script.remove_sonos_from_speaker_group
data:
target_player: media_player.living_room_sonos
# Pause this room speaker in case it is the last item in the group
# or music follower is off
# Remove this room speaker from the group
- service: script.pause_sonos_if_sole_speaker_group
data:
target_player: media_player.living_room_sonos
Maintaince on master speaker. states:
###############################################
#
# Sonos Music state mantainance
#
###############################################
- alias: S- Set Music Master Speaker When The Only One Is Playing
trigger:
- platform: state
entity_id:
- media_player.living_room_sonos
- media_player.master_room_sonos
- media_player.kitchen_sonos
- media_player.first_corridor_sonos
to:
- "idle"
- "playing"
- "paused"
- "off"
- minutes: /5
platform: time_pattern
condition: []
action:
- choose:
# IF - only kitchen sonos is playing - set it as master speaker
- conditions:
- condition: state
state: "playing"
entity_id: media_player.kitchen_sonos
- condition: not
conditions:
- condition: state
state: "playing"
entity_id: media_player.living_room_sonos
- condition: state
state: "playing"
entity_id: media_player.master_room_sonos
- condition: state
state: "playing"
entity_id: media_player.first_corridor_sonos
sequence:
- service: input_select.select_option
entity_id: input_select.music_controller
data:
option: kitchen_sonos
# ELIF only living room sonos is playing music instead of TV - set it as master speaker
- conditions:
- condition: template
value_template: >
{% if state_attr('media_player.living_room_sonos', 'media_title') != 'TV' %}
true
{% else %}
false
{% endif %}
- condition: state
state: "playing"
entity_id: media_player.living_room_sonos
- condition: not
conditions:
- condition: state
state: "playing"
entity_id: media_player.kitchen_sonos
- condition: state
state: "playing"
entity_id: media_player.master_room_sonos
- condition: state
state: "playing"
entity_id: media_player.first_corridor_sonos
sequence:
- service: input_select.select_option
entity_id: input_select.music_controller
data:
option: living_room_sonos
# ELIF only master room sonos is playing - set it as master speaker
- conditions:
- condition: state
state: "playing"
entity_id: media_player.master_room_sonos
- condition: not
conditions:
- condition: state
state: "playing"
entity_id: media_player.living_room_sonos
- condition: state
state: "playing"
entity_id: media_player.kitchen_sonos
- condition: state
state: "playing"
entity_id: media_player.first_corridor_sonos
sequence:
- service: input_select.select_option
entity_id: input_select.music_controller
data:
option: master_room_sonos
# ELIF only first corridor sonos is playing - set it as master speaker
- conditions:
- condition: state
state: "playing"
entity_id: media_player.first_corridor_sonos
- condition: not
conditions:
- condition: state
state: "playing"
entity_id: media_player.living_room_sonos
- condition: state
state: "playing"
entity_id: media_player.kitchen_sonos
- condition: state
state: "playing"
entity_id: media_player.master_room_sonos
sequence:
- service: input_select.select_option
entity_id: input_select.music_controller
data:
option: first_corridor_sonos
You can find these scripts/automation in my configuration: GitHub - relliky/My_HASSIO_Config: My Home Assistant Automation Configuration
This works very well for me but I am having trouble to write it to blueprints for sharing as I normally customize each room slightly differently and there are many customized variables which makes it hard to write blueprints. Hope this could help you guys if you want to do such thing.