Change template loop detection strategy to allow self-referencing updates when there are multiple templates (#39943)
This commit is contained in:
parent
e746965b1c
commit
aaa8083d49
3 changed files with 220 additions and 39 deletions
|
@ -5,6 +5,7 @@ from typing import Any, Callable, List, Optional, Union
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback
|
from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
@ -121,7 +122,6 @@ class TemplateEntity(Entity):
|
||||||
"""Template Entity."""
|
"""Template Entity."""
|
||||||
self._template_attrs = {}
|
self._template_attrs = {}
|
||||||
self._async_update = None
|
self._async_update = None
|
||||||
self._async_update_entity_ids_filter = None
|
|
||||||
self._attribute_templates = attribute_templates
|
self._attribute_templates = attribute_templates
|
||||||
self._attributes = {}
|
self._attributes = {}
|
||||||
self._availability_template = availability_template
|
self._availability_template = availability_template
|
||||||
|
@ -130,6 +130,7 @@ class TemplateEntity(Entity):
|
||||||
self._entity_picture_template = entity_picture_template
|
self._entity_picture_template = entity_picture_template
|
||||||
self._icon = None
|
self._icon = None
|
||||||
self._entity_picture = None
|
self._entity_picture = None
|
||||||
|
self._self_ref_update_count = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
|
@ -223,18 +224,34 @@ class TemplateEntity(Entity):
|
||||||
updates: List[TrackTemplateResult],
|
updates: List[TrackTemplateResult],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Call back the results to the attributes."""
|
"""Call back the results to the attributes."""
|
||||||
|
|
||||||
if event:
|
if event:
|
||||||
self.async_set_context(event.context)
|
self.async_set_context(event.context)
|
||||||
|
|
||||||
|
entity_id = event and event.data.get(ATTR_ENTITY_ID)
|
||||||
|
|
||||||
|
if entity_id and entity_id == self.entity_id:
|
||||||
|
self._self_ref_update_count += 1
|
||||||
|
else:
|
||||||
|
self._self_ref_update_count = 0
|
||||||
|
|
||||||
|
# If we need to make this less sensitive in the future,
|
||||||
|
# change the '>=' to a '>' here.
|
||||||
|
if self._self_ref_update_count >= len(self._template_attrs):
|
||||||
|
for update in updates:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Template loop detected while processing event: %s, skipping template render for Template[%s]",
|
||||||
|
event,
|
||||||
|
update.template.template,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
for update in updates:
|
for update in updates:
|
||||||
for attr in self._template_attrs[update.template]:
|
for attr in self._template_attrs[update.template]:
|
||||||
attr.handle_result(
|
attr.handle_result(
|
||||||
event, update.template, update.last_result, update.result
|
event, update.template, update.last_result, update.result
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._async_update_entity_ids_filter:
|
|
||||||
self._async_update_entity_ids_filter({self.entity_id})
|
|
||||||
|
|
||||||
if self._async_update:
|
if self._async_update:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@ -249,12 +266,8 @@ class TemplateEntity(Entity):
|
||||||
)
|
)
|
||||||
self.async_on_remove(result_info.async_remove)
|
self.async_on_remove(result_info.async_remove)
|
||||||
result_info.async_refresh()
|
result_info.async_refresh()
|
||||||
result_info.async_update_entity_ids_filter({self.entity_id})
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
self._async_update = result_info.async_refresh
|
self._async_update = result_info.async_refresh
|
||||||
self._async_update_entity_ids_filter = (
|
|
||||||
result_info.async_update_entity_ids_filter
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Run when entity about to be added to hass."""
|
"""Run when entity about to be added to hass."""
|
||||||
|
|
|
@ -562,7 +562,6 @@ class _TrackTemplateResultInfo:
|
||||||
self._info: Dict[Template, RenderInfo] = {}
|
self._info: Dict[Template, RenderInfo] = {}
|
||||||
self._last_domains: Set = set()
|
self._last_domains: Set = set()
|
||||||
self._last_entities: Set = set()
|
self._last_entities: Set = set()
|
||||||
self._entity_ids_filter: Set = set()
|
|
||||||
|
|
||||||
def async_setup(self) -> None:
|
def async_setup(self) -> None:
|
||||||
"""Activation of template tracking."""
|
"""Activation of template tracking."""
|
||||||
|
@ -723,27 +722,12 @@ class _TrackTemplateResultInfo:
|
||||||
"""Force recalculate the template."""
|
"""Force recalculate the template."""
|
||||||
self._refresh(None)
|
self._refresh(None)
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_update_entity_ids_filter(self, entity_ids: Set) -> None:
|
|
||||||
"""Update the filtered entity_ids."""
|
|
||||||
self._entity_ids_filter = entity_ids
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _refresh(self, event: Optional[Event]) -> None:
|
def _refresh(self, event: Optional[Event]) -> None:
|
||||||
entity_id = event and event.data.get(ATTR_ENTITY_ID)
|
entity_id = event and event.data.get(ATTR_ENTITY_ID)
|
||||||
updates = []
|
updates = []
|
||||||
info_changed = False
|
info_changed = False
|
||||||
|
|
||||||
if entity_id and entity_id in self._entity_ids_filter:
|
|
||||||
# Skip self-referencing updates
|
|
||||||
for track_template_ in self._track_templates:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Template loop detected while processing event: %s, skipping template render for Template[%s]",
|
|
||||||
event,
|
|
||||||
track_template_.template.template,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
for track_template_ in self._track_templates:
|
for track_template_ in self._track_templates:
|
||||||
template = track_template_.template
|
template = track_template_.template
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -4,6 +4,8 @@ from unittest.mock import patch
|
||||||
|
|
||||||
from homeassistant.bootstrap import async_from_config_dict
|
from homeassistant.bootstrap import async_from_config_dict
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_PICTURE,
|
||||||
|
ATTR_ICON,
|
||||||
EVENT_COMPONENT_LOADED,
|
EVENT_COMPONENT_LOADED,
|
||||||
EVENT_HOMEASSISTANT_START,
|
EVENT_HOMEASSISTANT_START,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
|
@ -763,12 +765,6 @@ async def test_sun_renders_once_per_sensor(hass):
|
||||||
async def test_self_referencing_sensor_loop(hass, caplog):
|
async def test_self_referencing_sensor_loop(hass, caplog):
|
||||||
"""Test a self referencing sensor does not loop forever."""
|
"""Test a self referencing sensor does not loop forever."""
|
||||||
|
|
||||||
template_str = """
|
|
||||||
{% for state in states -%}
|
|
||||||
{{ state.last_updated }}
|
|
||||||
{%- endfor %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
await async_setup_component(
|
await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"sensor",
|
"sensor",
|
||||||
|
@ -777,7 +773,7 @@ async def test_self_referencing_sensor_loop(hass, caplog):
|
||||||
"platform": "template",
|
"platform": "template",
|
||||||
"sensors": {
|
"sensors": {
|
||||||
"test": {
|
"test": {
|
||||||
"value_template": template_str,
|
"value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -790,15 +786,203 @@ async def test_self_referencing_sensor_loop(hass, caplog):
|
||||||
|
|
||||||
assert len(hass.states.async_all()) == 1
|
assert len(hass.states.async_all()) == 1
|
||||||
|
|
||||||
value = hass.states.get("sensor.test").state
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
value2 = hass.states.get("sensor.test").state
|
|
||||||
assert value2 == value
|
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
value3 = hass.states.get("sensor.test").state
|
|
||||||
assert value3 == value2
|
|
||||||
|
|
||||||
assert "Template loop detected" in caplog.text
|
assert "Template loop detected" in caplog.text
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.test")
|
||||||
|
assert int(state.state) == 1
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert int(state.state) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_self_referencing_sensor_with_icon_loop(hass, caplog):
|
||||||
|
"""Test a self referencing sensor loops forever with a valid self referencing icon."""
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"sensor",
|
||||||
|
{
|
||||||
|
"sensor": {
|
||||||
|
"platform": "template",
|
||||||
|
"sensors": {
|
||||||
|
"test": {
|
||||||
|
"value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
|
||||||
|
"icon_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}mdi:greater{% else %}mdi:less{% endif %}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_start()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) == 1
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "Template loop detected" in caplog.text
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.test")
|
||||||
|
assert int(state.state) == 2
|
||||||
|
assert state.attributes[ATTR_ICON] == "mdi:greater"
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert int(state.state) == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_self_referencing_sensor_with_icon_and_picture_entity_loop(hass, caplog):
|
||||||
|
"""Test a self referencing sensor loop forevers with a valid self referencing icon."""
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"sensor",
|
||||||
|
{
|
||||||
|
"sensor": {
|
||||||
|
"platform": "template",
|
||||||
|
"sensors": {
|
||||||
|
"test": {
|
||||||
|
"value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
|
||||||
|
"icon_template": "{% if ((states.sensor.test.state or 0) | int) > 3 %}mdi:greater{% else %}mdi:less{% endif %}",
|
||||||
|
"entity_picture_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}bigpic{% else %}smallpic{% endif %}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_start()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) == 1
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "Template loop detected" in caplog.text
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.test")
|
||||||
|
assert int(state.state) == 3
|
||||||
|
assert state.attributes[ATTR_ICON] == "mdi:less"
|
||||||
|
assert state.attributes[ATTR_ENTITY_PICTURE] == "bigpic"
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert int(state.state) == 3
|
||||||
|
|
||||||
|
|
||||||
|
async def test_self_referencing_entity_picture_loop(hass, caplog):
|
||||||
|
"""Test a self referencing sensor does not loop forever with a looping self referencing entity picture."""
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"sensor",
|
||||||
|
{
|
||||||
|
"sensor": {
|
||||||
|
"platform": "template",
|
||||||
|
"sensors": {
|
||||||
|
"test": {
|
||||||
|
"value_template": "{{ 1 }}",
|
||||||
|
"entity_picture_template": "{{ ((states.sensor.test.attributes['entity_picture'] or 0) | int) + 1 }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_start()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) == 1
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "Template loop detected" in caplog.text
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.test")
|
||||||
|
assert int(state.state) == 1
|
||||||
|
assert state.attributes[ATTR_ENTITY_PICTURE] == "1"
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert int(state.state) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_self_referencing_icon_with_no_loop(hass, caplog):
|
||||||
|
"""Test a self referencing icon that does not loop."""
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.heartworm_high_80", 10)
|
||||||
|
hass.states.async_set("sensor.heartworm_low_57", 10)
|
||||||
|
hass.states.async_set("sensor.heartworm_avg_64", 10)
|
||||||
|
hass.states.async_set("sensor.heartworm_avg_57", 10)
|
||||||
|
|
||||||
|
value_template_str = """{% if (states.sensor.heartworm_high_80.state|int >= 10) and (states.sensor.heartworm_low_57.state|int >= 10) %}
|
||||||
|
extreme
|
||||||
|
{% elif (states.sensor.heartworm_avg_64.state|int >= 30) %}
|
||||||
|
high
|
||||||
|
{% elif (states.sensor.heartworm_avg_64.state|int >= 14) %}
|
||||||
|
moderate
|
||||||
|
{% elif (states.sensor.heartworm_avg_64.state|int >= 5) %}
|
||||||
|
slight
|
||||||
|
{% elif (states.sensor.heartworm_avg_57.state|int >= 5) %}
|
||||||
|
marginal
|
||||||
|
{% elif (states.sensor.heartworm_avg_57.state|int < 5) %}
|
||||||
|
none
|
||||||
|
{% endif %}"""
|
||||||
|
|
||||||
|
icon_template_str = """{% if is_state('sensor.heartworm_risk',"extreme") %}
|
||||||
|
mdi:hazard-lights
|
||||||
|
{% elif is_state('sensor.heartworm_risk',"high") %}
|
||||||
|
mdi:triangle-outline
|
||||||
|
{% elif is_state('sensor.heartworm_risk',"moderate") %}
|
||||||
|
mdi:alert-circle-outline
|
||||||
|
{% elif is_state('sensor.heartworm_risk',"slight") %}
|
||||||
|
mdi:exclamation
|
||||||
|
{% elif is_state('sensor.heartworm_risk',"marginal") %}
|
||||||
|
mdi:heart
|
||||||
|
{% elif is_state('sensor.heartworm_risk',"none") %}
|
||||||
|
mdi:snowflake
|
||||||
|
{% endif %}"""
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"sensor",
|
||||||
|
{
|
||||||
|
"sensor": {
|
||||||
|
"platform": "template",
|
||||||
|
"sensors": {
|
||||||
|
"heartworm_risk": {
|
||||||
|
"value_template": value_template_str,
|
||||||
|
"icon_template": icon_template_str,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_start()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) == 5
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.heartworm_high_80", 10)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "Template loop detected" not in caplog.text
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.heartworm_risk")
|
||||||
|
assert state.state == "extreme"
|
||||||
|
assert state.attributes[ATTR_ICON] == "mdi:hazard-lights"
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert state.state == "extreme"
|
||||||
|
assert state.attributes[ATTR_ICON] == "mdi:hazard-lights"
|
||||||
|
assert "Template loop detected" not in caplog.text
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue