Avoid `states` and `states.<domain>` in state-based template entities

TL;DR

A state-based template entity whose template iterates over states or states.<domain> can silently stop updating. Move that iteration into a trigger-based template entity (or an automation, group or label) and let state-based entities consume an explicit list of entity IDs.

Symptoms

  • A template sensor that aggregates over many entities (counts, lists, “any unavailable?”) goes stale and stays wrong, even after the underlying entities change again.
  • Your log contains Template loop detected warnings for that entity, although there’s no real loop.

Why it happens

HA’s template tracking decides when to re-render a template based on what it references:

Reference Re-renders when Rate limit
states('sensor.foo'), states.sensor.foo, is_state(...) that entity changes none
states.switch | ... any entity in the domain changes 1 per second
states | ... any entity at all changes 1 per minute
now() every minute —

Filters after states.<domain> don’t narrow tracking: the whole domain is still referenced. So a template switch using states.switch references itself, even if you filter it out, and so does any template sensor using states.

Template entities have loop detection: if two consecutive re-renders are attributed to the entity itself changing, the render is aborted. Combined with rate limiting, a self-attributed state change can end up “holding” the rate-limit timer, so the next render is attributed to the entity itself, loop detection aborts it, and the updates received in that window are lost. Because the internal “last result” was already updated, the entity may stay out of sync afterwards.

Whether or not that’s a bug, the practical advice from the core team is: don’t use states or states.<domain> in state-based template entities. In practice they converge to the rate limit anyway, so you lose nothing by being explicit.

Rule of thumb

  • Fine: states/states.<domain> in automations, scripts and trigger-based template entities (as long as the entity isn’t in its own triggers).
  • Fine: Single-entity access anywhere: states('sensor.x'), states.sensor.x.name, states['sensor.x'].
  • Avoid: states or states.<domain> in state-based template entities, especially when the entity is in the domain it iterates over.

Recipes

The examples below track battery sensors (entity IDs ending in _battery) and count those below 20%. The start and reload triggers make trigger-based entities populate immediately upon a template config reload or HA restart instead of waiting up to a minute afterwards.

1. Trigger-based entity, once a minute

Same effective behaviour as states | ..., without the loop detection.

template:
  - triggers:
      - trigger: homeassistant
        event: start
      - trigger: event
        event_type: event_template_reloaded
      - trigger: time_pattern
        minutes: /1
    sensor:
      - name: Low batteries
        unique_id: low_batteries
        state: >
          {{ states.sensor
             | selectattr('entity_id', 'search', '_battery$')
             | map(attribute='state')
             | map('float', 100)
             | select('lt', 20)
             | list | count }}

2. Trigger-based entity list + state-based consumer (live updates)

The trigger-based entity rebuilds the membership list once a minute. The state-based entity references each entity individually, so it updates live with no rate limit.

template:
  - triggers:
      - trigger: homeassistant
        event: start
      - trigger: event
        event_type: event_template_reloaded
      - trigger: time_pattern
        minutes: /1
    sensor:
      - name: Battery sensor list
        unique_id: battery_sensor_list
        state: >
          {{ states.sensor | selectattr('entity_id', 'search', '_battery$') | list | count }}
        attributes:
          entities: >
            {{ states.sensor
               | selectattr('entity_id', 'search', '_battery$')
               | map(attribute='entity_id')
               | list }}

  - sensor:
      - name: Low batteries
        unique_id: low_batteries
        state: >
          {{ state_attr('sensor.battery_sensor_list', 'entities')
             | default([], true)
             | map('states')
             | map('float', 100)
             | select('lt', 20)
             | list | count }}

Store entity IDs, not state objects, in the attribute. For large lists, consider excluding the list sensor from the recorder.

3. Group maintained by an automation

group.set groups don’t survive a restart, hence the start trigger.

automation:
  - alias: Maintain battery sensor group
    triggers:
      - trigger: homeassistant
        event: start
      - trigger: time_pattern
        minutes: /1
    actions:
      - action: group.set
        data:
          object_id: battery_sensors
          entities: >
            {{ states.sensor
               | selectattr('entity_id', 'search', '_battery$')
               | map(attribute='entity_id')
               | list }}

template:
  - sensor:
      - name: Low batteries
        unique_id: low_batteries
        state: >
          {{ expand('group.battery_sensors')
             | map(attribute='state')
             | map('float', 100)
             | select('lt', 20)
             | list | count }}

4. Labels

Label the entities in the UI and skip the pattern matching entirely.

template:
  - sensor:
      - name: Low batteries
        unique_id: low_batteries
        state: >
          {{ label_entities('battery')
             | map('states')
             | map('float', 100)
             | select('lt', 20)
             | list | count }}

Adding or removing a label isn’t a state change, so membership changes are picked up on the next re-render.

Credits

Diagnosis by @redstone99 and guidance by @petro in Template entities referencing “states” can miss updates - diagnosed.

8 Likes

Thanks for sharing!

1 Like

The wording here can seem a little vague, I tripped over it for half a sec. Maybe something like, “The start and reload triggers make trigger-based entities populate immediately upon a template reload or HA restart instead of waiting up to a minute afterwards”. It’s a touch wordier but definitely more concise.

Elsewise nice writeup, I hope it saves people some frustration. Personally I’d highlight recipe #4, I started going the label route some time ago and it has really simplified things for me… especially when it comes to template filtering.

1 Like

Thanks! Edit made. :slight_smile: Remember that anybody can edit a community guide.

EDIT: Oops, just fixed the category. It wasn’t marked as a community guide.

1 Like