Fix template entity preview when templates error (#154029)

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Petro31
2025-10-27 11:53:36 -04:00
committed by GitHub
parent 84065767d1
commit e3359fb62d
2 changed files with 110 additions and 18 deletions

View File

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

View File

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