Group Sonos and Ungroup Sonos Based on Presence - Scripts & Automations

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

  1. group with speaker when enterring a new room
  2. ungroup the speaker and pause the music when leaving the room for a while
  3. maintainance on master speaker
  4. pause the music if people leaves all the room that have sonos speakers
  5. 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.

7 Likes

More details on hardware you used for presence detection in each room etc

Hi David, I think any PIR sensors would do the job. The key to this project is how to automate this process in my opinion.

But if you are interested, I am using Xiaomi Motion Sensor (zigbee version) which is cheapest & smallest motion sensor I can find and it does not uses WIFI so it does not put pressure on my wifi APs.

I am using room assistant

Hi,
I write exactly the same kind of automation with Home Assistant and I use the first player of the sonos_group list as group master which make things much easier :slight_smile:
My only issue yet is that if the group master leave the group (if I switch room), the whole group stop which is not what I want. I see in your automatation that the group master never leave the group, I was wondering if you had a better workaround for that now?

I write exactly the same kind of automation with Home Assistant and I use the first player of the sonos_group list as group master which make things much easier :slight_smile:

I did the same thing. However, I improved it by setting up the second player of the sonos_group list as group master if I leave the room that is the current group master. So the group master actually leave the group. See the code of " # Update the master speaker in the group" for details

Thanks for this, but one question from a noob :slight_smile:

Where do I put all this code? I already realized the Sonos integration with the mini media player, but I am not really satisfied as the functionality is really minimized.

You need to put these as part of your automation.yaml and script.yaml and include them from your configuration.yaml like this

automation: !include automation.yaml
script: !include script.yaml

See Automation YAML - Home Assistant for reference.

You will also need to update my automation with your devices but I think you can directly use the script.

1 Like

does this still work with 2022.5? Since this update they removed sonos_group and now my own way of grouping (or at least, detecting the group) is not working anymore, and since this has been a long time since I have set it up, I am not sure how to fix this :confused:

I had this done with a custom sensor that used the sonos_group as value_template:

          {%- for player in states.media_player if player.attributes.sonos_group %}
          {%- if player.attributes.sonos_group[0] == player.entity_id %}
            {%- set slaves = player.attributes.sonos_group[1:] %}
            {{ slaves|join(",")|replace("media_player", "input_boolean") }}
          {%- endif %}
          {%- endfor %}

but in 2022.5 they changed sonos_group for group_members, and somehow neither now work with this…

The sonos_group attribute was renamed and is now called group_members. Adjust your template accordingly.

{%- for player in states.media_player if player.attributes.group_members is defined %}
  {%- if player.attributes.group_members[0] == player.entity_id %}
    {%- set slaves = player.attributes.group_members[1:] %}
    {{ slaves|join(",")|replace("media_player", "input_boolean") }}
  {%- endif %}
{%- endfor %}

thx! I tried that but it didn’t work, but seems I was missing the is defined part :slight_smile:

If you have non-Sonos media_players, states.media_player will include them. However, you will get an error if you attempt to access the group_members attribute for a media_player that doesn’t have it. That’s why the for reduces the list of media_players to only those that have the group_members attribute defined.

{%- for player in states.media_player if player.attributes.group_members is defined %}
                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Another way of doing the same thing:

{%- for player in states.media_player | selectattr('attributes.group_members', 'defined') | list %}
1 Like

thx for the explanation!

Thanks guys for spotting this as well. I just updated to this month release and realised this automation was broken due to this.

Hey @relliky , is your config on github working after the update? Mine has gone to crap and i can’t workout why so will probably start from scratch if its working for you

Thanks :slight_smile:

The latest release, version 2022.8.0, eliminated service calls sonos.join and sonos.unjoin. The replacements are media_player.join and media_player.unjoin.

The script in the original post needs modification in order to work with 2022.8.0.

Hey @123 i had already updated the media_player.join options but still not wokring anymore. Im getting an error in the logs saying:

S-KC Ensuite Group Its Speaker If People Present: Error executing script. Invalid data for call_service at pos 1: extra keys not allowed @ data[‘master’]

script:
  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.bedroom"
    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') != 'workshop' and target_player == 'media_player.workshop' %}
                {{ state.attributes.volume_level + 0.10 }}
              {% elif states('input_select.music_controller') == 'workshop' and target_player != 'media_player.workshop' %}
                {{ state.attributes.volume_level - 0.10 }}
              {% 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: media_player.join
        data_template:
          master: media_player.{{ states('input_select.music_controller') }}
          entity_id: >
            {% if target_player is not none %}
              {{ target_player }}
            {% else %}
              media_player.lounge
            {% endif %}
      - service: media_player.join
        data_template:
          master: media_player.{{ states('input_select.music_controller') }}
          entity_id: >
            {% if target_player is not none %}
              {{ target_player }}
            {% else %}
              media_player.lounge
            {% endif %}

  - alias: S-KC Kitchen Group Its Speaker If People Present
    id: "1560305343071"
    description: "automation.s_kc_kitchen_group_its_speaker_if_people_present"
    trigger:
      - platform: state
        to: "on"
        entity_id: group.kitchen
    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.kitchen
      - service: automation.turn_off
        entity_id: automation.s_kc_kitchen_group_its_speaker_if_people_present

Any ideas whats going on here?
Thanks :slight_smile:

The problem is identified in the error message:

extra keys not allowed @ data[‘master’]

master is not a valid option for media_player.join. You’re using the options that were valid for sonos.join but they’re different for media_player.join.

service: media_player.join
target:
  entity_id: "media_player.{{ states('input_select.music_controller') }}"
data:
  group_members: "{{ target_player if target_player is not none else media_player.lounge }}"

That worked! Thanks heaps :smiley:

Hello friends,

I hope you can help me. I tried to install the whole thing at HA.

I have adapted the script to my player as far as I can see. However, I keep getting an error when I run it. I’m fairly new to HA.

###########################################
# 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 %}


    # Now join the player into the group twice in case sometimes it didn't manage to join in for certain cases
    - service: media_player.join
      data_template:
        master: media_player.{{ states('input_select.music_controller') }}
        entity_id: >
          {% if target_player is not none %}
            {{ target_player }}
          {% else %}
            media_player.kuche
          {% endif %}

    - service: media_player.join
      data_template:
        master: media_player.{{ states('input_select.music_controller') }}
        entity_id: >
          {% if target_player is not none %}
            {{ target_player }}
          {% else %}
            media_player.kuche
          {% 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, "group_members") %}
            {% 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: media_player.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.küche
          {% 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, "group_members") %}
          {% if loop.index == 1 %}  
            {% if loop.length == 1 %} 
              true  
            {% else %}      
              false      
            {% endif %}
          {% endif %}
        {% endfor %}

    - service: media_player.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.kuche
          {% endif %}

alias: test123
description: ""
trigger:
  - platform: state
    entity_id:
      - input_boolean.4
    to: "on"
    id: test an
condition: []
action:
  - service: script.add_sonos_into_speaker_group
      data:
        target_player: media_player.bad
mode: single
extra keys not allowed @ data['master']. Got None required key not provided @ data['group_members']. Got None

thank you