Exclude Sonos speaker that is running on battery

I’ve created an automation that synchronizes the volume levels for all Sonos speakers in the same sonos_group. This is done by checking the sonos_group members of the speaker that triggers the automation. To simulate the trigger, I start my template in the editor with:

{% set trigger = namespace(entity_id="media_player.<speaker>") %}

Now I want to exclude the speaker (Move or Roam) that is not on its charging dock. So I create an “reject array” like this:

{% set undocked = namespace(entities=[]) %}
{% for state in state_attr(trigger.entity_id,"sonos_group") if is_state("binary_sensor."+state.split(".")[1]+"_power", "off") %}
{% set undocked.entities = undocked.entities + [ state ] %}
{%- endfor %}

It’s a bit creative, but since it’s not an attribute, I cannot think of an easier way. It however works like a charm and can be validated by entering {{ undocked.entities }}.

Now I want to reject the entities in “undocked.entities” by doing this:

{{ state_attr(trigger.entity_id,"sonos_group")|rejectattr("entity_id","in",undocked.entities)|list }}

And you guessed it, this doesn’t work and I can’t figure out why…

If I test it manually by "media_player.<speaker that’s in array>" in undocked.entities and it works like expected. It’s probably something small, but I’m probably overlooking it. Anyone any idea?


Edit: I solved it like the post below shows. @123 (Taras) nevertheless explained why the rejectattr didn’t work.

Solved it by making the filter an ‘if not’. Now I don’t have to do the reject anymore. It however remains a question why the rejectattr didn’t work.

{% for state in state_attr(trigger.entity_id,"sonos_group") if not is_state("binary_sensor."+state.split(".")[1]+"_power", "off") %}
     {% set undocked.entities = undocked.entities + [ state ] %}
{%- endfor %} 

Use expand() to convert the list of media_player entity_ids (in sonos_group) to a generator object containing each media_player’s complete information. Then rejectattr is able to do its work properly.

{{ expand(state_attr(trigger.entity_id, 'sonos_group'))
  | rejectattr('entity_id', 'in', undocked.entities)
  | map(attribute='entity_id') | list }}

Hmmm… interesting. I tried expand, but (if I remember correctly) I had map() in between expand and reject and not after the reject.

One to keep in mind, thanks!

If you did this:

map(attribute='entity_id')

it stripped away all other information and the only thing remaining is the value of the entity_id key. You have the rejectattr filter looking for a key named entity_id but the map filter discarded all keys.

This would have worked:

{{ expand(state_attr(trigger.entity_id, 'sonos_group'))
  | map(attribute='entity_id') | reject('in', undocked.entities) | list }}
1 Like

That’s what I think I tried more or less. Your reject('in', ...) is new to me, so for sure it wasn’t exactly the same. I think my code was something like:

{{ expand(state_attr(trigger.entity_id, 'sonos_group'))
  | map(attribute='entity_id') | rejectattr('entity_id', 'in', undocked.entities) | list }}

If it was like that then it failed for the reasons I explained in my previous post. There’s no entity_id key remaining after the map filter.

Exactly, so I learned something new today.
Thanks for that :slight_smile:

1 Like

I did a little test in the meantime and f you use reject you don’t even need to expand.
This works too:

{{ state_attr(trigger.entity_id,"sonos_group")|reject("in",undocked.entities)|list }}

Absolutely correct. I winged that second template and simply copy-pasted the first one without giving any thought about expand. :man_facepalming: I think you made it as concise as it’s going to get. :+1:


Just for fun, and only if you have the time, see if this produces the same results as your first template that creates a “reject array”.

{{ state_attr(trigger.entity_id, 'sonos_group') | map('device_id')
  | map('device_entities') | map('select', 'search', '_power$')
  | map('join') | expand | selectattr('state', 'eq', 'off') 
  | map(attribute='entity_id') | list }}

Honestly, it’s no better than the for-loop you’re using (arguably longer and more complex) but, simply as an exercise for myself, I wanted to see if I could do it without a for-loop.

  • Gets the entity_ids of the sonos group
  • Finds each entity_id’s device_id
  • Finds the entity_id of all entities associated with each device_id
  • Selects only the entity_ids ending with _power
  • Joins the result to produce a single list of entities (they should be binary_sensors)
  • Expands each entity_id
  • Selects only the entity_ids that are off
  • Collapses it to a list of (media_player) entity_ids

NOTE
I don’t have any Sonos Roam devices so I couldn’t confirm it works.

It works perfectly :slight_smile:
Interesting, also for future code. It keeps surprising (in a positive way).
Good to see search supports regex too.

Since I need a list of entities multiple times in one automation (choose condition and the actual “action”) I made it a variable that has all entities I “need”:

variables:
  target_ids: >-
    {% set target = namespace(entities=[]) %} {% for state in
    state_attr(trigger.entity_id,"sonos_group") if not
    is_state("binary_sensor."+state.split(".")[1]+"_power", "off") %}
      {% set target.entities = target.entities + [ state ] %}
    {%- endfor %} {{ target.entities }}

It’s a good practice; many of my automations employ variables. Just keep in mind (to avoid frustration later) that a variable cannot store a generator object. In other words, the result of expand(whatever) cannot be stored directly in a variable.

Good to know indeed :+1:
Inspired by your code, I’ve found the way back from power to the media_player. Needed to sync the volume once it’s back on it’s dock again :slight_smile:

{{ device_entities(device_id(trigger.entity_id)) 
   | select("search","^media_player") | list
}}

The trigger of course is the change to on (equals “charging”)

I call it a day.