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
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -121,7 +122,6 @@ class TemplateEntity(Entity):
|
|||
"""Template Entity."""
|
||||
self._template_attrs = {}
|
||||
self._async_update = None
|
||||
self._async_update_entity_ids_filter = None
|
||||
self._attribute_templates = attribute_templates
|
||||
self._attributes = {}
|
||||
self._availability_template = availability_template
|
||||
|
@ -130,6 +130,7 @@ class TemplateEntity(Entity):
|
|||
self._entity_picture_template = entity_picture_template
|
||||
self._icon = None
|
||||
self._entity_picture = None
|
||||
self._self_ref_update_count = 0
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -223,18 +224,34 @@ class TemplateEntity(Entity):
|
|||
updates: List[TrackTemplateResult],
|
||||
) -> None:
|
||||
"""Call back the results to the attributes."""
|
||||
|
||||
if event:
|
||||
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 attr in self._template_attrs[update.template]:
|
||||
attr.handle_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:
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
@ -249,12 +266,8 @@ class TemplateEntity(Entity):
|
|||
)
|
||||
self.async_on_remove(result_info.async_remove)
|
||||
result_info.async_refresh()
|
||||
result_info.async_update_entity_ids_filter({self.entity_id})
|
||||
self.async_write_ha_state()
|
||||
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:
|
||||
"""Run when entity about to be added to hass."""
|
||||
|
|
|
@ -562,7 +562,6 @@ class _TrackTemplateResultInfo:
|
|||
self._info: Dict[Template, RenderInfo] = {}
|
||||
self._last_domains: Set = set()
|
||||
self._last_entities: Set = set()
|
||||
self._entity_ids_filter: Set = set()
|
||||
|
||||
def async_setup(self) -> None:
|
||||
"""Activation of template tracking."""
|
||||
|
@ -723,27 +722,12 @@ class _TrackTemplateResultInfo:
|
|||
"""Force recalculate the template."""
|
||||
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
|
||||
def _refresh(self, event: Optional[Event]) -> None:
|
||||
entity_id = event and event.data.get(ATTR_ENTITY_ID)
|
||||
updates = []
|
||||
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:
|
||||
template = track_template_.template
|
||||
if (
|
||||
|
|
|
@ -4,6 +4,8 @@ from unittest.mock import patch
|
|||
|
||||
from homeassistant.bootstrap import async_from_config_dict
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_ICON,
|
||||
EVENT_COMPONENT_LOADED,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
STATE_OFF,
|
||||
|
@ -763,12 +765,6 @@ async def test_sun_renders_once_per_sensor(hass):
|
|||
async def test_self_referencing_sensor_loop(hass, caplog):
|
||||
"""Test a self referencing sensor does not loop forever."""
|
||||
|
||||
template_str = """
|
||||
{% for state in states -%}
|
||||
{{ state.last_updated }}
|
||||
{%- endfor %}
|
||||
"""
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"sensor",
|
||||
|
@ -777,7 +773,7 @@ async def test_self_referencing_sensor_loop(hass, caplog):
|
|||
"platform": "template",
|
||||
"sensors": {
|
||||
"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
|
||||
|
||||
value = hass.states.get("sensor.test").state
|
||||
await hass.async_block_till_done()
|
||||
|
||||
value2 = hass.states.get("sensor.test").state
|
||||
assert value2 == value
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
value3 = hass.states.get("sensor.test").state
|
||||
assert value3 == value2
|
||||
|
||||
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
Reference in a new issue