diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 2e42b28fbd2..632eeea8926 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -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.""" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 8e126c7c14c..733214749ef 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -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 ( diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 439f154b4af..6c9bfa7e632 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -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