Change template loop detection strategy to allow self-referencing updates when there are multiple templates (#39943)

This commit is contained in:
J. Nick Koston 2020-09-12 07:20:21 -05:00 committed by GitHub
parent e746965b1c
commit aaa8083d49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 220 additions and 39 deletions

View file

@ -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."""

View file

@ -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 (

View file

@ -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