Skip to content

Commit e3359fb

Browse files
Petro31emontnemery
andauthored
Fix template entity preview when templates error (home-assistant#154029)
Co-authored-by: Erik Montnemery <[email protected]>
1 parent 8406576 commit e3359fb

File tree

2 files changed

+110
-18
lines changed

2 files changed

+110
-18
lines changed

homeassistant/components/template/template_entity.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,16 +344,23 @@ def _handle_results(
344344
)
345345
return
346346

347+
errors = []
347348
for update in updates:
348349
for template_attr in self._template_attrs[update.template]:
349350
template_attr.handle_result(
350351
event, update.template, update.last_result, update.result
351352
)
353+
if isinstance(update.result, TemplateError):
354+
errors.append(update.result)
352355

353356
if not self._preview_callback:
354357
self.async_write_ha_state()
355358
return
356359

360+
if errors:
361+
self._preview_callback(None, None, None, str(errors[-1]))
362+
return
363+
357364
try:
358365
calculated_state = self._async_calculate_state()
359366
validate_state(calculated_state.state)
@@ -451,13 +458,19 @@ def async_start_preview(
451458
) -> CALLBACK_TYPE:
452459
"""Render a preview."""
453460

454-
def log_template_error(level: int, msg: str) -> None:
455-
preview_callback(None, None, None, msg)
461+
def suppress_preview_errors(level: int, msg: str) -> None:
462+
"""Suppress redundant template render errors.
463+
464+
Preview entities render templates at least 3 times before the preview entity
465+
is created. If template contains an error, each render will produce an error.
466+
Instead of overwhelming the client with errors, suppress them and raise
467+
a single error through the self._handle_results method.
468+
"""
456469

457470
self._preview_callback = preview_callback
458471
self._async_setup_templates()
459472
try:
460-
self._async_template_startup(None, log_template_error)
473+
self._async_template_startup(None, suppress_preview_errors)
461474
except Exception as err: # noqa: BLE001
462475
preview_callback(None, None, None, str(err))
463476
return self._call_on_remove_callbacks

tests/components/template/test_config_flow.py

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
from homeassistant import config_entries
1010
from homeassistant.components.template import DOMAIN, async_setup_entry
11-
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
11+
from homeassistant.config_entries import SOURCE_USER
12+
from homeassistant.const import STATE_UNAVAILABLE
1213
from homeassistant.core import HomeAssistant
1314
from homeassistant.data_entry_flow import FlowResultType
1415
from homeassistant.helpers import device_registry as dr
@@ -872,10 +873,10 @@ async def test_options(
872873
),
873874
(
874875
"sensor",
875-
"{{ float(states('sensor.one'), default='') + float(states('sensor.two'), default='') }}",
876+
"{{ float(states('sensor.one'), 0.0) + float(states('sensor.two'), 0.0) }}",
876877
{},
877878
{"one": "30.0", "two": "20.0"},
878-
["", STATE_UNKNOWN, "50.0"],
879+
["0.0", "30.0", "50.0"],
879880
[{}, {}],
880881
[["one", "two"], ["one", "two"]],
881882
),
@@ -1124,7 +1125,7 @@ async def test_config_flow_preview_bad_input(
11241125
"sensor",
11251126
"{{ float(states('sensor.one')) + float(states('sensor.two')) }}",
11261127
{"one": "30.0", "two": "20.0"},
1127-
["unavailable", "50.0"],
1128+
["50.0"],
11281129
[
11291130
(
11301131
"ValueError: Template error: float got invalid input 'unknown' "
@@ -1181,18 +1182,14 @@ async def test_config_flow_preview_template_startup_error(
11811182
assert msg["type"] == "event"
11821183
assert msg["event"] == {"error": error_event}
11831184

1184-
msg = await client.receive_json()
1185-
assert msg["type"] == "event"
1186-
assert msg["event"]["state"] == template_states[0]
1187-
11881185
for input_entity in input_entities:
11891186
hass.states.async_set(
11901187
f"{template_type}.{input_entity}", input_states[input_entity], {}
11911188
)
11921189

11931190
msg = await client.receive_json()
11941191
assert msg["type"] == "event"
1195-
assert msg["event"]["state"] == template_states[1]
1192+
assert msg["event"]["state"] == template_states[0]
11961193

11971194

11981195
@pytest.mark.parametrize(
@@ -1208,8 +1205,8 @@ async def test_config_flow_preview_template_startup_error(
12081205
"sensor",
12091206
"{{ float(states('sensor.one')) > 30 and undefined_function() }}",
12101207
[{"one": "30.0", "two": "20.0"}, {"one": "35.0", "two": "20.0"}],
1211-
["False", "unavailable"],
1212-
["'undefined_function' is undefined"],
1208+
["False"],
1209+
["UndefinedError: 'undefined_function' is undefined"],
12131210
),
12141211
],
12151212
)
@@ -1273,10 +1270,6 @@ async def test_config_flow_preview_template_error(
12731270
assert msg["type"] == "event"
12741271
assert msg["event"] == {"error": error_event}
12751272

1276-
msg = await client.receive_json()
1277-
assert msg["type"] == "event"
1278-
assert msg["event"]["state"] == template_states[1]
1279-
12801273

12811274
@pytest.mark.parametrize(
12821275
(
@@ -1749,3 +1742,89 @@ async def test_options_flow_change_device(
17491742
**state_template,
17501743
**extra_options,
17511744
}
1745+
1746+
1747+
@pytest.mark.parametrize(
1748+
("step_id", "user_input", "expected_error"),
1749+
[
1750+
(
1751+
"light",
1752+
{
1753+
"name": "",
1754+
"state": "{{ state() }}",
1755+
"level": "{{ statex() }}",
1756+
"turn_on": [],
1757+
"turn_off": [],
1758+
"set_level": [],
1759+
},
1760+
"UndefinedError: 'statex' is undefined",
1761+
),
1762+
(
1763+
"sensor",
1764+
{
1765+
"name": "",
1766+
"state": "{{ state() }}",
1767+
},
1768+
"UndefinedError: 'state' is undefined",
1769+
),
1770+
(
1771+
"light",
1772+
{
1773+
"name": "",
1774+
"state": "{{ state() }}",
1775+
"level": "{{ states('sensor.abc') }}",
1776+
"turn_on": [],
1777+
"turn_off": [],
1778+
"set_level": [],
1779+
},
1780+
"UndefinedError: 'state' is undefined",
1781+
),
1782+
],
1783+
)
1784+
async def test_preview_error(
1785+
hass: HomeAssistant,
1786+
hass_ws_client: WebSocketGenerator,
1787+
step_id: str,
1788+
user_input: dict,
1789+
expected_error: str,
1790+
) -> None:
1791+
"""Test preview will error if any template errors."""
1792+
client = await hass_ws_client(hass)
1793+
1794+
result = await hass.config_entries.flow.async_init(
1795+
DOMAIN, context={"source": SOURCE_USER}
1796+
)
1797+
assert result["type"] is FlowResultType.MENU
1798+
1799+
result = await hass.config_entries.flow.async_configure(
1800+
result["flow_id"],
1801+
{"next_step_id": step_id},
1802+
)
1803+
await hass.async_block_till_done()
1804+
1805+
assert result["type"] is FlowResultType.FORM
1806+
assert result["step_id"] == step_id
1807+
assert result["errors"] is None
1808+
assert result["preview"] == "template"
1809+
1810+
await client.send_json_auto_id(
1811+
{
1812+
"type": "template/start_preview",
1813+
"flow_id": result["flow_id"],
1814+
"flow_type": "config_flow",
1815+
"user_input": user_input,
1816+
}
1817+
)
1818+
msg = await client.receive_json()
1819+
1820+
assert msg["success"]
1821+
assert msg["result"] is None
1822+
1823+
# Test expected error
1824+
msg = await client.receive_json()
1825+
assert "error" in msg["event"]
1826+
assert msg["event"]["error"] == expected_error
1827+
1828+
# Test No preview is created
1829+
with pytest.raises(TimeoutError):
1830+
await client.receive_json(timeout=0.01)

0 commit comments

Comments
 (0)