diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d9fdce8b1f3..1404b3730f1 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -789,7 +789,32 @@ class _TrackTemplateResultInfo: def async_setup(self, raise_on_template_error: bool, strict: bool = False) -> None: """Activation of template tracking.""" + block_render = False + super_template = self._track_templates[0] if self._has_super_template else None + + # Render the super template first + if super_template is not None: + template = super_template.template + variables = super_template.variables + self._info[template] = info = template.async_render_to_info( + variables, strict=strict + ) + + # If the super template did not render to True, don't update other templates + try: + super_result: str | TemplateError = info.result() + except TemplateError as ex: + super_result = ex + if ( + super_result is not None + and self._super_template_as_boolean(super_result) is not True + ): + block_render = True + + # Then update the remaining templates unless blocked by the super template for track_template_ in self._track_templates: + if block_render or track_template_ == super_template: + continue template = track_template_.template variables = track_template_.variables self._info[template] = info = template.async_render_to_info( @@ -810,9 +835,10 @@ class _TrackTemplateResultInfo: ) self._update_time_listeners() _LOGGER.debug( - "Template group %s listens for %s", + "Template group %s listens for %s, first render blocker by super template: %s", self._track_templates, self.listeners, + block_render, ) @property @@ -932,6 +958,14 @@ class _TrackTemplateResultInfo: return TrackTemplateResult(template, last_result, result) + @staticmethod + def _super_template_as_boolean(result: bool | str | TemplateError) -> bool: + """Return True if the result is truthy or a TemplateError.""" + if isinstance(result, TemplateError): + return True + + return result_as_boolean(result) + @callback def _refresh( self, @@ -974,13 +1008,6 @@ class _TrackTemplateResultInfo: track_templates = track_templates or self._track_templates - def _super_template_as_boolean(result: bool | str | TemplateError) -> bool: - """Return True if the result is truthy or a TemplateError.""" - if isinstance(result, TemplateError): - return True - - return result_as_boolean(result) - # Update the super template first if super_template is not None: update = self._render_template_if_ready(super_template, now, event) @@ -994,14 +1021,14 @@ class _TrackTemplateResultInfo: # If the super template did not render to True, don't update other templates if ( super_result is not None - and _super_template_as_boolean(super_result) is not True + and self._super_template_as_boolean(super_result) is not True ): block_updates = True if ( isinstance(update, TrackTemplateResult) - and _super_template_as_boolean(update.last_result) is not True - and _super_template_as_boolean(update.result) is True + and self._super_template_as_boolean(update.last_result) is not True + and self._super_template_as_boolean(update.result) is True ): # Super template changed from not True to True, force re-render # of all templates in the group @@ -1030,9 +1057,10 @@ class _TrackTemplateResultInfo: ) ) _LOGGER.debug( - "Template group %s listens for %s", + "Template group %s listens for %s, re-render blocker by super template: %s", self._track_templates, self.listeners, + block_updates, ) if not updates: diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 6d78ae089da..4f62d50da34 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1233,6 +1233,151 @@ async def test_track_template_result_super_template(hass): assert len(wildercard_runs_availability) == 6 +async def test_track_template_result_super_template_initially_false(hass): + """Test tracking template with super template listening to same entity.""" + specific_runs = [] + specific_runs_availability = [] + wildcard_runs = [] + wildcard_runs_availability = [] + wildercard_runs = [] + wildercard_runs_availability = [] + + template_availability = Template("{{ is_number(states('sensor.test')) }}", hass) + template_condition = Template("{{states.sensor.test.state}}", hass) + template_condition_var = Template( + "{{(states.sensor.test.state|int) + test }}", hass + ) + + # Make the super template initially false + hass.states.async_set("sensor.test", "unavailable") + await hass.async_block_till_done() + + def specific_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition: + specific_runs.append(int(track_result.result)) + elif track_result.template is template_availability: + specific_runs_availability.append(track_result.result) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition, None), + ], + specific_run_callback, + has_super_template=True, + ) + + @ha.callback + def wildcard_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition: + wildcard_runs.append( + (int(track_result.last_result or 0), int(track_result.result)) + ) + elif track_result.template is template_availability: + wildcard_runs_availability.append(track_result.result) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition, None), + ], + wildcard_run_callback, + has_super_template=True, + ) + + async def wildercard_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition_var: + wildercard_runs.append( + (int(track_result.last_result or 0), int(track_result.result)) + ) + elif track_result.template is template_availability: + wildercard_runs_availability.append(track_result.result) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition_var, {"test": 5}), + ], + wildercard_run_callback, + has_super_template=True, + ) + await hass.async_block_till_done() + + assert specific_runs_availability == [] + assert wildcard_runs_availability == [] + assert wildercard_runs_availability == [] + assert specific_runs == [] + assert wildcard_runs == [] + assert wildercard_runs == [] + + hass.states.async_set("sensor.test", 5) + await hass.async_block_till_done() + + assert specific_runs_availability == [True] + assert wildcard_runs_availability == [True] + assert wildercard_runs_availability == [True] + assert specific_runs == [5] + assert wildcard_runs == [(0, 5)] + assert wildercard_runs == [(0, 10)] + + hass.states.async_set("sensor.test", "unknown") + await hass.async_block_till_done() + + assert specific_runs_availability == [True, False] + assert wildcard_runs_availability == [True, False] + assert wildercard_runs_availability == [True, False] + + hass.states.async_set("sensor.test", 30) + await hass.async_block_till_done() + + assert specific_runs_availability == [True, False, True] + assert wildcard_runs_availability == [True, False, True] + assert wildercard_runs_availability == [True, False, True] + + assert specific_runs == [5, 30] + assert wildcard_runs == [(0, 5), (5, 30)] + assert wildercard_runs == [(0, 10), (10, 35)] + + hass.states.async_set("sensor.test", "other") + await hass.async_block_till_done() + + hass.states.async_set("sensor.test", 30) + await hass.async_block_till_done() + + assert len(specific_runs) == 2 + assert len(wildcard_runs) == 2 + assert len(wildercard_runs) == 2 + assert len(specific_runs_availability) == 5 + assert len(wildcard_runs_availability) == 5 + assert len(wildercard_runs_availability) == 5 + + hass.states.async_set("sensor.test", 30) + await hass.async_block_till_done() + + assert len(specific_runs) == 2 + assert len(wildcard_runs) == 2 + assert len(wildercard_runs) == 2 + assert len(specific_runs_availability) == 5 + assert len(wildcard_runs_availability) == 5 + assert len(wildercard_runs_availability) == 5 + + hass.states.async_set("sensor.test", 31) + await hass.async_block_till_done() + + assert len(specific_runs) == 3 + assert len(wildcard_runs) == 3 + assert len(wildercard_runs) == 3 + assert len(specific_runs_availability) == 5 + assert len(wildcard_runs_availability) == 5 + assert len(wildercard_runs_availability) == 5 + + @pytest.mark.parametrize( "availability_template", [ @@ -1370,6 +1515,145 @@ async def test_track_template_result_super_template_2(hass, availability_templat assert wildercard_runs == [(0, 10), (10, 35)] +@pytest.mark.parametrize( + "availability_template", + [ + "{{ states('sensor.test2') != 'unavailable' }}", + "{% if states('sensor.test2') != 'unavailable' -%} true {%- else -%} false {%- endif %}", + "{% if states('sensor.test2') != 'unavailable' -%} 1 {%- else -%} 0 {%- endif %}", + "{% if states('sensor.test2') != 'unavailable' -%} yes {%- else -%} no {%- endif %}", + "{% if states('sensor.test2') != 'unavailable' -%} on {%- else -%} off {%- endif %}", + "{% if states('sensor.test2') != 'unavailable' -%} enable {%- else -%} disable {%- endif %}", + # This will throw when sensor.test2 is not "unavailable" + "{% if states('sensor.test2') != 'unavailable' -%} {{'a' + 5}} {%- else -%} false {%- endif %}", + ], +) +async def test_track_template_result_super_template_2_initially_false( + hass, availability_template +): + """Test tracking template with super template listening to different entities.""" + specific_runs = [] + specific_runs_availability = [] + wildcard_runs = [] + wildcard_runs_availability = [] + wildercard_runs = [] + wildercard_runs_availability = [] + + template_availability = Template(availability_template) + template_condition = Template("{{states.sensor.test.state}}", hass) + template_condition_var = Template( + "{{(states.sensor.test.state|int) + test }}", hass + ) + + hass.states.async_set("sensor.test2", "unavailable") + await hass.async_block_till_done() + + def _super_template_as_boolean(result): + if isinstance(result, TemplateError): + return True + + return result_as_boolean(result) + + def specific_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition: + specific_runs.append(int(track_result.result)) + elif track_result.template is template_availability: + specific_runs_availability.append( + _super_template_as_boolean(track_result.result) + ) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition, None), + ], + specific_run_callback, + has_super_template=True, + ) + + @ha.callback + def wildcard_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition: + wildcard_runs.append( + (int(track_result.last_result or 0), int(track_result.result)) + ) + elif track_result.template is template_availability: + wildcard_runs_availability.append( + _super_template_as_boolean(track_result.result) + ) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition, None), + ], + wildcard_run_callback, + has_super_template=True, + ) + + async def wildercard_run_callback(event, updates): + for track_result in updates: + if track_result.template is template_condition_var: + wildercard_runs.append( + (int(track_result.last_result or 0), int(track_result.result)) + ) + elif track_result.template is template_availability: + wildercard_runs_availability.append( + _super_template_as_boolean(track_result.result) + ) + + async_track_template_result( + hass, + [ + TrackTemplate(template_availability, None), + TrackTemplate(template_condition_var, {"test": 5}), + ], + wildercard_run_callback, + has_super_template=True, + ) + await hass.async_block_till_done() + + assert specific_runs_availability == [] + assert wildcard_runs_availability == [] + assert wildercard_runs_availability == [] + assert specific_runs == [] + assert wildcard_runs == [] + assert wildercard_runs == [] + + hass.states.async_set("sensor.test", 5) + hass.states.async_set("sensor.test2", "available") + await hass.async_block_till_done() + + assert specific_runs_availability == [True] + assert wildcard_runs_availability == [True] + assert wildercard_runs_availability == [True] + assert specific_runs == [5] + assert wildcard_runs == [(0, 5)] + assert wildercard_runs == [(0, 10)] + + hass.states.async_set("sensor.test2", "unknown") + await hass.async_block_till_done() + + assert specific_runs_availability == [True] + assert wildcard_runs_availability == [True] + assert wildercard_runs_availability == [True] + + hass.states.async_set("sensor.test2", "available") + hass.states.async_set("sensor.test", 30) + await hass.async_block_till_done() + + assert specific_runs_availability == [True] + assert wildcard_runs_availability == [True] + assert wildercard_runs_availability == [True] + assert specific_runs == [5, 30] + assert wildcard_runs == [(0, 5), (5, 30)] + assert wildercard_runs == [(0, 10), (10, 35)] + + async def test_track_template_result_complex(hass): """Test tracking template.""" specific_runs = []