From cefba7c638badc3bb37e4f2db34c044c923edf41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Mar 2023 10:50:34 -1000 Subject: [PATCH] Avoid falling back to listening for all states when a template render raises an exception (#89392) When a template render raised an exception we would start listening for all states until the template did not raise an exception anymore. This was not needed since the entity that is causing the exception was already in the tracker. Re-rendering on all state changes can be extremely expensive and can bring an instance into a sluggish or unresponsive state when updating from a much older version that did not raise ValueError when a default was missing. --- homeassistant/helpers/event.py | 10 +++--- homeassistant/helpers/template.py | 2 ++ tests/helpers/test_event.py | 52 ++++++++++++++++++++++++++++++- tests/helpers/test_template.py | 11 +++++++ 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index edbb5fa7354..3ac715426e3 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -838,6 +838,10 @@ class TrackTemplateResultInfo: self._track_state_changes: _TrackStateChangeFiltered | None = None self._time_listeners: dict[Template, Callable[[], None]] = {} + def __repr__(self) -> str: + """Return the representation.""" + return f"" + def async_setup(self, raise_on_template_error: bool, strict: bool = False) -> None: """Activation of template tracking.""" block_render = False @@ -1651,12 +1655,6 @@ def _render_infos_needs_all_listener(render_infos: Iterable[RenderInfo]) -> bool if render_info.all_states or render_info.all_states_lifecycle: return True - # Previous call had an exception - # so we do not know which states - # to track - if render_info.exception: - return True - return False diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 2e112706fba..c923bd2d84a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -274,6 +274,8 @@ class RenderInfo: f" entities={self.entities}" f" rate_limit={self.rate_limit}" f" has_time={self.has_time}" + f" exception={self.exception}" + f" is_static={self.is_static}" ">" ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index fb0925c15d2..066460c90d8 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -2209,7 +2209,7 @@ async def test_track_template_result_errors( hass.states.async_set("switch.not_exist", "on") await hass.async_block_till_done() - assert len(syntax_error_runs) == 1 + assert len(syntax_error_runs) == 0 assert len(not_exist_runs) == 2 assert not_exist_runs[1][0].data.get("entity_id") == "switch.not_exist" assert not_exist_runs[1][1] == template_not_exist @@ -2229,6 +2229,56 @@ async def test_track_template_result_errors( assert isinstance(not_exist_runs[2][3], TemplateError) +async def test_track_template_result_transient_errors( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test tracking template with transient errors in the template.""" + hass.states.async_set("sensor.error", "unknown") + template_that_raises_sometimes = Template( + "{{ states('sensor.error') | float }}", hass + ) + + sometimes_error_runs = [] + + @ha.callback + def sometimes_error_listener(event, updates): + track_result = updates.pop() + sometimes_error_runs.append( + ( + event, + track_result.template, + track_result.last_result, + track_result.result, + ) + ) + + info = async_track_template_result( + hass, + [TrackTemplate(template_that_raises_sometimes, None)], + sometimes_error_listener, + ) + await hass.async_block_till_done() + + assert sometimes_error_runs == [] + assert "ValueError" in caplog.text + assert "ValueError" in repr(info) + caplog.clear() + + hass.states.async_set("sensor.error", "unavailable") + await hass.async_block_till_done() + assert len(sometimes_error_runs) == 1 + assert isinstance(sometimes_error_runs[0][3], TemplateError) + sometimes_error_runs.clear() + assert "ValueError" in repr(info) + + hass.states.async_set("sensor.error", "4") + await hass.async_block_till_done() + assert len(sometimes_error_runs) == 1 + assert sometimes_error_runs[0][3] == 4.0 + sometimes_error_runs.clear() + assert "ValueError" not in repr(info) + + async def test_static_string(hass: HomeAssistant) -> None: """Test a static string.""" template_refresh = Template("{{ 'static' }}", hass) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 2434b2aed15..f97f0a4b9c5 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -4323,3 +4323,14 @@ def test_contains(hass: HomeAssistant, seq, value, expected) -> None: ) == expected ) + + +async def test_render_to_info_with_exception(hass: HomeAssistant) -> None: + """Test info is still available if the template has an exception.""" + hass.states.async_set("test_domain.object", "dog") + info = render_to_info(hass, '{{ states("test_domain.object") | float }}') + with pytest.raises(TemplateError, match="no default was specified"): + info.result() + + assert info.all_states is False + assert info.entities == {"test_domain.object"}