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 detectedwarnings 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:
statesorstates.<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.