Jinja2 Expert Help Needed

It’s OK. Thanks for your help anyway.

If you want loop.last to work properly I believe you need to add a filter to your for loop.

Something like

{% for entity in states.group.entrypoints.attributes.entity_id if states(entity, 'open') %}
  {% if loop.last %}, and {% elif loop.first %}{% else %}, {% endif %}
  {{ states[entity.split('.')[0]][entity.split('.')[1]].name }}
{% endfor %}
2 Likes

Wow! Thanks. It works with this…

{% for entity in states.group.entrypoints.attributes.entity_id if is_state(entity, 'open') %}
{%- if loop.last %} and {% elif loop.first %}{% else %}, {% endif -%}
{{ states[entity.split('.')[0]][entity.split('.')[1]].name }}
{%- endfor %}

@nordlead2005

How to count the number of entities that has the state open?

I wish to have something like this…

If only one entity in the loop, it outputs something like… “Front Door is open”.

If more than one entity… “Front Door, Master Bedroom Window B and Balcony Door are open”

if there are 10 entities in the loop… “All doors and windows are open

I’m not really jinja expert really, so there may be a better way than below.

For the first scenario, just check if loop.first & loop.last
For the second scenario, do what we were doing before but add that last flavor text with another loop.last check
For the last, check the loop.length vs the length of the collection.

{% for entity in states.group.entrypoints.attributes.entity_id if states(entity, 'open') %}
  {% if loop.first and loop.last %}
    {{ states[entity.split('.')[0]][entity.split('.')[1]].name }} is open
  {% elif loop.length == length(states.group.entrypoints.attributes.entity_id) %}
    All doors and windows are open
    {% break %}
  {% else %}
    {% if loop.last %}, and {% elif loop.first %}{% else %}, {% endif %}
    {{ states[entity.split('.')[0]][entity.split('.')[1]].name }}
    {{ "are open" if loop.last }}
  {% endif %}
{% endfor %}

One note, is that in the developer tab {% break %} and length() don’t work, I do not know if it works elsewhere but according to the jinja2 documentation it should exist.

The length function can easily be replaced by a magic number (in other words, 10), but the break would be messy to replace. You’d have to do something like check loop.length in every case and add a check for loop.first when printing “All…” to prevent it from printing multiple times.

1 Like

Thanks for your help. From there, I manage to make it happen with this piece of code…

{% for entity in states.group.entrypoints.attributes.entity_id if is_state(entity, 'open') %}
  {%- if loop.first and loop.last %}
    {{ states[entity.split('.')[0]][entity.split('.')[1]].name }} is open
  {% elif loop.length == states.group.occupants.attributes.entity_id|length %}
    {{ "All doors and windows are open" if loop.last }}
  {% else %}
    {%- if loop.first %}{% elif loop.last %} & {% else %}, {% endif -%}
    {{ states[entity.split('.')[0]][entity.split('.')[1]].name }}{{ " are open" if loop.last }}
  {%- endif -%}
{% else %}
  All doors and windows are close
{% endfor %}
2 Likes

I have created my own sensor similar to this but it doesn’t auto update on the front end?

Am i missing something? I have to reboot Home Assistant to see a new value.

You need to add entity_id to the sensor. Example…

    entrypoints_open:
      friendly_name: 'Which entry points are open?'
      value_template: >-
        {% for entity in states.group.entrypoints.attributes.entity_id if is_state(entity, 'open') %}
          {%- if loop.first and loop.last %}
            {{ states[entity.split('.')[0]][entity.split('.')[1]].name }} is open
          {% elif loop.length == states.group.occupants.attributes.entity_id|length %}
            {{ "All doors and windows are open" if loop.last }}
          {% else %}
            {%- if loop.first %}{% elif loop.last %} & {% else %}, {% endif -%}
            {{ states[entity.split('.')[0]][entity.split('.')[1]].name }}{{ " are open" if loop.last }}
          {%- endif -%}
        {% else %}
          All doors and windows are close
        {% endfor %}
      entity_id:
        - sensor.front_door
        - sensor.living_room_window
        - sensor.back_door
        - sensor.kitchen_window

Basically, the entity_id follows the same entity_id in your group.

1 Like

Thanks @masterkenobi looks like that solved it.

1 Like

Just for the sake of it being a challenge, I wrote a one-liner that lists all entities of a group (group.entrypoints) that currently have a specific state (open):

{{ dict((states|selectattr('entity_id', 'in', state_attr('group.entrypoints', 'entity_id'))|list)|groupby('state'))['open']|map(attribute='name')|list|join(', ') }}
6 Likes

so cool, use this for seeing which of my motion sensors is sensing motion… :wink: and lights :+1:, and people home… the use is endless! You should create a component out of this… just fill in the group, and the filter as properties and your off. wow.

needed and discussed here: Groups behaviour of template sensors - #14 by petro

now count them and list them nicely…

count them:

{{ states | selectattr('entity_id', 'in', state_attr('group.entrypoints', 'entity_id')) | selectattr('state', 'equalto', 'open') | list | length }}
2 Likes

Thanks @dale3h and @petro, these are some nice one-liners! :grinning:

While doing a non-scientific speed comparison in the template editor, the first one feels a tad sluggish however as 2 dictionaries go through 2 type changes to lists. The second one is already a bit faster and to the point, but it still has to filter through the whole list of states.

When using this as a template sensor, since the sensor will only be updated based on changes to the group and not for each member entities, we still need to list these entities inside an entity_id property so that the sensor gets updated as we want. So we have a list of entities in the group, and we need to copy that list for the sensor. This somewhat defeats the purpose of automagically detecting which entity has changed state by just pointing to the group. Bummer.

Since we need to list the entities anyway, instead of scanning the whole list of states and mapping them against the members of the group, it becomes much faster to simply list the group’s entities and then scan for their state. No need for an entity_id property either, as the parser can already detect the entities named within the template.

This would then result in a very fast template sensor for group.entrypoints that would get updated whenever one of its entities’ state has changed:

- platform: template
  sensors:
    entrypoints_open:
      value_template: >-
        {% set entities = [
            states.sensor.front_door,
            states.sensor.living_room_window,
            states.sensor.back_door,
            states.sensor.kitchen_window,
            states.sensor.master_bedroom_window_a,
            states.sensor.master_bedroom_window_b,
            states.sensor.balcony_door,
          ]
        %}
        {{ entities|selectattr('state', 'equalto', 'open')
                   |map(attribute='name')
                   |join(', ') }}
3 Likes

The time difference on these should be negligible on just about any system. The delay you are seeing in template is because of how templates work.

Templates only update when an entity inside the equation used in a template updates. Because you are only using 1 entity (group.entrypoints), the template will only update when it see’s changes in the group. Not when changes occur to the groups entities.

So in your method, you list out all the states. That will be instant because the template is scanning for changes in all the underlying entities.

How would you modify {{ entities|selectattr('state', 'equalto', 'open') to match an attribute instead of the state?

I’m with you on the mechanics of template parsing! :wink:

The delay I perceive when testing on a Raspberry Pi is 0.5 to 1 seconds when using the entity_id sensor property and scanning the group with a sensor setup as follows:

- platform: template
  sensors:
    entrypoints_group_open:
      entity_id:
        - sensor.front_door
        - sensor.living_room_window
        - sensor.back_door
        - sensor.kitchen_window
        - sensor.master_bedroom_window_a
        - sensor.master_bedroom_window_b
        - sensor.balcony_door
      value_template: >-
        {% states|selectattr('entity_id', 'in', state_attr('group.entrypoints', 'entity_id'))
                 |selectattr('state', 'equalto', 'on')
                 |map(attribute='name')
                 |join(', ') %}

…versus almost instant by just scanning the entities. No biggie for most sensors, scripts or automations, but some time-sensitive implementations would benefit from the reduced delay (say, play a chime when opening a door).

{{ entities|selectattr('attributes.icon', 'equalto', 'mdi:door') }} or other more relevant attributes work, but the parser won’t update this template if the entity’s state hasn’t changed.

nice!
could you enhance this with a filter showing name, and another attributes, more specifically last_changed?
Im trying this template for finding all lights on, and their last_changed attribute…

  {{ states|selectattr('entity_id', 'in', state_attr('group.all_lights_only', 'entity_id'))
                 |selectattr('state', 'equalto', 'on')
                 |map(attribute='last_changed')
                 |join(', ') }}  

gives last_changed for all on lights alright but id like it to show attributes ‘name’ : ‘last_changed’

Your best case scenario is to make 2 lists and iterate througth them. You can’t map 2 values.

{% set mystates = states|selectattr('entity_id', 'in', state_attr('group.all_lights_only', 'entity_id')) |selectattr('state', 'equalto', 'on') | list %}
{% set names = mystates | map(attribute='name') | list %}
{% set times = mystates | map(attribute='last_updated') | list %}
{% for i in range(mystates | length) %}
{{ names[i] }} updated at {{ times[i] }}
{% endfor %}

there really isn’t any way to shorten it without making custom filters. No clue how to make them inside the HA environment.

1 Like

cool, working nicely!
made a small adjustment to make the timestamp more readable and in time with the local timezone, and tidy-up the listing, otherwise: into the cookbook it goes!

{% set mystates = states|selectattr('entity_id', 'in', state_attr('group.all_lights_only', 'entity_id')) |selectattr('state', 'equalto', 'on') | list %}
{% set names = mystates | map(attribute='name') | list %}
{% set times = mystates | map(attribute='last_updated') | list %}
{% for i in range(mystates | length) %}
{{ names[i] }} updated at {{ as_timestamp(times[i])|timestamp_custom("%d %b: %X") }}
{%- endfor %}

thanks.

1 Like