Store, retrieve and handle list of entities?

This is a follow-up question to


Dear Taras @123,

unfortunately, I am not getting further with the handling of the list you proposed here. I temporarily store the list of media player entities in an input_text helper so I know which media players to unmute once the advertisement is over, e.g.:

['media_player.bad', 'media_player.michael']

Its content will be generated by a binary_sensor with the exact syntax of your proposal and then stored in the input_text helper. This works already.

I have made several attempts to get it going but I failed so far in handling the list. In my automation I would like to iterate the list with a toggle mechanism like this:

# ⚠️ Does not work!

- variables:
	muted_players: >
	  {{ states('input_text.media_players_with_advertisements') }}

- repeat:
    count: '{{ muted_players | count }}'

    sequence:

    - service: media_player.volume_mute
      target:
        entity_id: '{{ muted_players[repeat.index].entity_id }}'
      data:
        is_volume_muted: "{{ is_state('binary_sensor.media_player_plays_advertisement', 'on') }}"

I guess the muted_players object needs a certain casting to work. Maybe the list has to be stored in a different way. Can you give me a hint, please? Thank you!

An entity’s state value always contains a string. So even though your input_text’s state value contains this:

['media_player.bad', 'media_player.michael']

it’s handled as a string, not a list. That’s why this doesn’t work the way you expected:

count: '{{ muted_players | count }}'

It’s not counting the number of items in a list but the number of characters in a string.

The process of converting a string that looks like a list into an actual list isn’t complicated but kind of awkward. I suggest you store the entity_id’s, in the input_text, simply separated by commas. That’s easy to convert to a true list afterwards.

For example, the template you currently use to create the list of entity_id’s probably ends with the list filter. Simply add the join filter to the end of it.

{{ blablabla | list | join(', ') }}

A comma and a space are used to separate each entity_id.

Afterwards, to convert the input_text’s state value to an actual list, we simply need to use the split method like this:

- variables:
	muted_players: >
	  {{ states('input_text.media_players_with_advertisements').split(', ') }}
1 Like

Thank you for your detailed explanation!

Unfortunately, I am not able to get the whole thing running as intended. I seem to have some challenges regarding the correct sequence syntax. But this is another topic.

Another option is to work with JSON objects. Paste this into the template editor and see what’s happening:

{% set x = "['media_player.bad', 'media_player.michael']" %}
string: {{ x }}
#chars: {{ x|count }}
quoted: {{ x|replace("'",'"') }}
object: {{ x|replace("'",'"')|from_json }}
#items: {{ x|replace("'",'"')|from_json|count }}
---
{% set y = x|replace("'",'"')|from_json -%}
{% for e in y -%}
{{e }}: {{ states(e) }}
{% endfor %}

A JSON list looks like this:

["item 1", "item 2"]

where the items should be enclosed in double-quotes.

In your original attempt, this:

      target:
        entity_id: '{{ muted_players[repeat.index].entity_id }}'

wasn’t going to work even if muted_players was an indexable list of entity IDs — you just needed:

      target:
        entity_id: '{{ muted_players[repeat.index - 1] }}'
1 Like

Thank you, @Troon, for your additional remarks! They have been helpful so far in regards of doing the counting. This is a step forward!

But…

… this is still an issue because I fail to expand a sensor like

sensor.media_player_players_with_advertisements

which actually contains a string like

['media_player.bad', 'media_player.michael']

to an iterable list. I tried to use your JSON workaround but unfortunately I was not able to implement the mix of single and double quotes in a single-quoted statement…

As it is a string I guess it needs to be cast back to a list somehow. Within an automation, I would like to get something like this working:

sequence:

  - repeat:
    
      count: "{{ states('sensor.media_player_players_with_advertisements_count') | int }}"
    
      sequence:
    
        - service: media_player.volume_mute
          target:
            entity_id: '{{ states("sensor.media_player_players_with_advertisements")[repeat.index - 1] }}'
          data:
            is_volume_muted: "on"

  - ...

So how can I use the JSON approach (or something different) for the line

entity_id: '{{ states("sensor.media_player_players_with_advertisements")[repeat.index - 1] }}'

to get an entity from that string above? Thank you!

Did you try my suggestion to store the entity_ids as a comma-delimited string using:

list | join(', ')

and then retrieving them as a list with:

.split(', ')

Just so we are all on the same page, post the code for whatever is responsible for writing the value to the input_text.

If that gives you difficulty, an alternative replacement strategy is to use the translate method to change ' (char 39) to " (char 34):

{% set x = "['media_player.bad', 'media_player.michael']" %}
{% set x_as_list = x.translate({39:34})|from_json %}
{% for i in range(x_as_list|count) -%}
{{ x_as_list[i] }}: {{ states(x_as_list[i]) }}
{% endfor %} 
1 Like

Sorry, I stopped using the input_text helper to reduce the possibility of errors. So I am using direct sensor information now like this:

template:
  - sensor:
    - name: 'Media Player: Players with advertisement'
      state: >
        {{ states.media_player
            | selectattr('attributes.media_title', 'defined')
            | selectattr('attributes.media_title', 'search', '<SIGNALWORD>')
            | map(attribute='entity_id') | list }}

This is also because I realised that the numbers of players affected are not recognized at once.

If you’re allergic to storing the entity_ids as a comma-separated string :slightly_smiling_face: and prefer the “kind of awkward” conversion I had alluded to earlier, it’s basically what troon suggested: convert the single quotes to double quotes to make it JSON compatible then filter it with from_json.

- variables:
	muted_players: >
	  {{ states('input_text.media_players_with_advertisements') | replace('\'','\"') | from_json }}
- repeat:
    count: '{{ muted_players | count }}'
    sequence:
    - service: media_player.volume_mute
      target:
        entity_id: '{{ muted_players[repeat.index-1] }}'
      data:
        is_volume_muted: "{{ is_state('binary_sensor.media_player_plays_advertisement', 'on') }}"

The reason I had called this approach “awkward” is because, in this case, there’s no advantage to storing the entity_ids with “list syntax”.

For starters, regardless of it’s list-like appearance, it’s handled as a string so all of the additional brackets and quotes just consume some of the limited space available in an entity’s state (255 characters max) yet don’t add value. In fact, the single quotes have to be replaced anyway so that the string (which only looks like a list) can be converted to a proper list with from_json.

In contrast, if you store the entity_ids as a comma-separated string, it contains only the bare minimum of extra characters to distinguish one entity_id from the next and makes it trivial to convert to a proper list.

Anyway, there you have it; the pros and cons of the two ways to do the same thing .

1 Like

If you store the list as an attribute it will be handled as a list.

template:
  - sensor:
    - name: 'Media Player: Players with advertisement'
      state: "{{ now().timestamp() | timestamp_custom() }}"
      attributes:
        players: >
          {{ states.media_player
            | selectattr('attributes.media_title', 'defined')
            | selectattr('attributes.media_title', 'search', '<SIGNALWORD>')
            | map(attribute='entity_id') | list }}

Therefore the attribute’s list can be used directly.

- variables:
	muted_players: >
	  {{ state_attr('input_text.media_players_with_advertisements', 'players') }}
- repeat:
    count: '{{ muted_players | count }}'
    sequence:
    - service: media_player.volume_mute
      target:
        entity_id: '{{ muted_players[repeat.index-1] }}'
      data:
        is_volume_muted: "{{ is_state('binary_sensor.media_player_plays_advertisement', 'on') }}"
2 Likes

Taras @123, your approach with the players attribute works so far which is excellent! (No, I am not allergic to anything. I am struggling with it and I just want it to work as I have already spent not just little time on this possibly minor coding challenge. :slight_smile:)

Now I am afraid I have to broaden the scope of this topic. Because in the meantime I have realised that there is another issue that makes it difficult to make my automation work. As mentioned

the sensor.media_players_with_advertisements may change several times while the automation is already in operation. That means, players are added (and finally removed) in between. I solved that with a while statement to repeat the muting:

- repeat:
    while:
      - condition: state
        entity_id: binary_sensor.media_player_plays_advertisement
        state: 'on'
    sequence:
    ...

But the real issue is that I need to know which players have been muted by the automation so I can unmute them once the advertisement is over. So within the repeat sequence of your code directly above, I just would like to add the player entity in the loop to a later iterable list again. Duplicates are not nice, but negligible. So it feels a bit like “back to square 1” now with the difference that this list is continuously self-built but not generated by an existing structure.

I already made some attempts like adding the player entities to a input_text (again…) but this does not seem to be reliable (apart from the duplicates and the problems with translating them back). So I hope there may be a better way?

Maybe you can recommend an way to build such a list which fits into your last proposal? Thank you!

I have evaluated both of your approaches, @Troon & @123, and and got both of them working in the Tempate Editor! :slight_smile:

Both approaches do the trick and are almost identical but the JSON approach requires and both approaches require additional coding if the input_text has an empty string. So I go for the “less awkward” approach with the comma-delimited string. (Also to prove that I am not allergic to it. :wink: )

Now I am able to store a list into an input_text and back like this:

{## Retrieve list from input_text ##}
{% set inputtext = "media_player.ABC, media_player.XYZ" %}
inputtext: {{ inputtext }}

{## add some players ##}
{% set inputtext_as_list = inputtext.split(', ') %}
{% set inputtext_as_list = inputtext_as_list + ['media_player.bad'] %}
{% set inputtext_as_list = inputtext_as_list + ['media_player.michael'] %}
inputtext_as_list: {{ inputtext_as_list }}

{## handle player entities ##}
{% for i in range(inputtext_as_list | count) -%}
  {{ inputtext_as_list[i] }}: {{ states(inputtext_as_list[i]) }}
{% endfor %}

{## store list in input_text ##}
{% set inputtext = inputtext_as_list | list | join(', ') %}
inputtext: {{ inputtext }}

{## handle empty list in input_text ##}
{% set inputtext = '' %}
{% set inputtext_as_list = inputtext.split(', ') %}
inputtext_as_list: {{ inputtext_as_list }}
{% for i in range(inputtext_as_list | count) -%}
  {{ inputtext_as_list[i] }}: {{ states(inputtext_as_list[i]) }}
{% endfor %}

Now I only have to put that into an automation and make it work. Thank you @123 and @Troon for your valuable support! :slight_smile:


EDIT:

OMG, I just realised that

inputtext.split(', ')

results in

['']

for an empty string. So what is missing there to really get an empty list…?

An empty string is considered to be logically false (a.k.a. “falsy”) so we can use that characteristic to determine if we should split it, to create a list, or simply use an empty list. Easy-peasy.

{## handle empty list in input_text ##}
{% set inputtext = '' %}
{% set inputtext_as_list = inputtext.split(', ') if inputtext else [] %}
inputtext_as_list: {{ inputtext_as_list }}
{% for i in range(inputtext_as_list | count) -%}
  {{ inputtext_as_list[i] }}: {{ states(inputtext_as_list[i]) }}
{% endfor %}

1 Like

Alternatives:

Removing any empty strings:

{{ inputtext.split(', ')|reject('eq','')|list }}

Using the falsiness description from above:

{{ inputtext.split(', ') if inputtext else [] }}

or slighter shorter for the code golfers:

{{ [inputtext.split(', '),[]][not inputtext] }}
1 Like

Reduced the par for this course by two strokes. :wink:

{{ iif(inputtext,inputtext.split(', '),[]) }}

EDIT

FWIW, the use of a comma and space to serve as the delimiter is purely for legibility but isn’t necessary for splitting the string. In fact, given that we’re dealing with entity_ids which never contain spaces, we can use a space character as the delimiter. It’s also the default delimiter for split. Therefore you can append this to create the space-separated string:

list | join(' ')

and this to convert it to a list:

{{ iif(inputtext,inputtext.split(),[]) }}
2 Likes

Hello code golfer pros,

hooray, I got it working yesterday! :partying_face: For a perfectionist like me, there’s certainly plenty of room for improvement, but for now I’m glad I made it thanks to your help. The refactoring can wait for now.

Thanks a million for your great support and your time @123 & @Troon! :slight_smile:

2 Likes