From e3359fb62de4ea73a2729a762032f08f0f59b39a Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:53:36 -0400 Subject: [PATCH] Fix template entity preview when templates error (#154029) Co-authored-by: Erik Montnemery --- .../components/template/template_entity.py | 19 ++- tests/components/template/test_config_flow.py | 109 +++++++++++++++--- 2 files changed, 110 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index ca3841dc961..834beaeb3fd 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -344,16 +344,23 @@ class TemplateEntity(AbstractTemplateEntity): ) return + errors = [] for update in updates: for template_attr in self._template_attrs[update.template]: template_attr.handle_result( event, update.template, update.last_result, update.result ) + if isinstance(update.result, TemplateError): + errors.append(update.result) if not self._preview_callback: self.async_write_ha_state() return + if errors: + self._preview_callback(None, None, None, str(errors[-1])) + return + try: calculated_state = self._async_calculate_state() validate_state(calculated_state.state) @@ -451,13 +458,19 @@ class TemplateEntity(AbstractTemplateEntity): ) -> CALLBACK_TYPE: """Render a preview.""" - def log_template_error(level: int, msg: str) -> None: - preview_callback(None, None, None, msg) + def suppress_preview_errors(level: int, msg: str) -> None: + """Suppress redundant template render errors. + + Preview entities render templates at least 3 times before the preview entity + is created. If template contains an error, each render will produce an error. + Instead of overwhelming the client with errors, suppress them and raise + a single error through the self._handle_results method. + """ self._preview_callback = preview_callback self._async_setup_templates() try: - self._async_template_startup(None, log_template_error) + self._async_template_startup(None, suppress_preview_errors) except Exception as err: # noqa: BLE001 preview_callback(None, None, None, str(err)) return self._call_on_remove_callbacks diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 3bf7b836a8b..08b087c2eca 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -8,7 +8,8 @@ from pytest_unordered import unordered from homeassistant import config_entries from homeassistant.components.template import DOMAIN, async_setup_entry -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr @@ -872,10 +873,10 @@ async def test_options( ), ( "sensor", - "{{ float(states('sensor.one'), default='') + float(states('sensor.two'), default='') }}", + "{{ float(states('sensor.one'), 0.0) + float(states('sensor.two'), 0.0) }}", {}, {"one": "30.0", "two": "20.0"}, - ["", STATE_UNKNOWN, "50.0"], + ["0.0", "30.0", "50.0"], [{}, {}], [["one", "two"], ["one", "two"]], ), @@ -1124,7 +1125,7 @@ async def test_config_flow_preview_bad_input( "sensor", "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", {"one": "30.0", "two": "20.0"}, - ["unavailable", "50.0"], + ["50.0"], [ ( "ValueError: Template error: float got invalid input 'unknown' " @@ -1181,10 +1182,6 @@ async def test_config_flow_preview_template_startup_error( assert msg["type"] == "event" assert msg["event"] == {"error": error_event} - msg = await client.receive_json() - assert msg["type"] == "event" - assert msg["event"]["state"] == template_states[0] - for input_entity in input_entities: hass.states.async_set( f"{template_type}.{input_entity}", input_states[input_entity], {} @@ -1192,7 +1189,7 @@ async def test_config_flow_preview_template_startup_error( msg = await client.receive_json() assert msg["type"] == "event" - assert msg["event"]["state"] == template_states[1] + assert msg["event"]["state"] == template_states[0] @pytest.mark.parametrize( @@ -1208,8 +1205,8 @@ async def test_config_flow_preview_template_startup_error( "sensor", "{{ float(states('sensor.one')) > 30 and undefined_function() }}", [{"one": "30.0", "two": "20.0"}, {"one": "35.0", "two": "20.0"}], - ["False", "unavailable"], - ["'undefined_function' is undefined"], + ["False"], + ["UndefinedError: 'undefined_function' is undefined"], ), ], ) @@ -1273,10 +1270,6 @@ async def test_config_flow_preview_template_error( assert msg["type"] == "event" assert msg["event"] == {"error": error_event} - msg = await client.receive_json() - assert msg["type"] == "event" - assert msg["event"]["state"] == template_states[1] - @pytest.mark.parametrize( ( @@ -1749,3 +1742,89 @@ async def test_options_flow_change_device( **state_template, **extra_options, } + + +@pytest.mark.parametrize( + ("step_id", "user_input", "expected_error"), + [ + ( + "light", + { + "name": "", + "state": "{{ state() }}", + "level": "{{ statex() }}", + "turn_on": [], + "turn_off": [], + "set_level": [], + }, + "UndefinedError: 'statex' is undefined", + ), + ( + "sensor", + { + "name": "", + "state": "{{ state() }}", + }, + "UndefinedError: 'state' is undefined", + ), + ( + "light", + { + "name": "", + "state": "{{ state() }}", + "level": "{{ states('sensor.abc') }}", + "turn_on": [], + "turn_off": [], + "set_level": [], + }, + "UndefinedError: 'state' is undefined", + ), + ], +) +async def test_preview_error( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + step_id: str, + user_input: dict, + expected_error: str, +) -> None: + """Test preview will error if any template errors.""" + client = await hass_ws_client(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": step_id}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == step_id + assert result["errors"] is None + assert result["preview"] == "template" + + await client.send_json_auto_id( + { + "type": "template/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": user_input, + } + ) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] is None + + # Test expected error + msg = await client.receive_json() + assert "error" in msg["event"] + assert msg["event"]["error"] == expected_error + + # Test No preview is created + with pytest.raises(TimeoutError): + await client.receive_json(timeout=0.01)