mirror of
https://github.com/home-assistant/core.git
synced 2025-11-09 19:09:32 +00:00
Fix template entity preview when templates error (#154029)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
@@ -344,16 +344,23 @@ class TemplateEntity(AbstractTemplateEntity):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
errors = []
|
||||||
for update in updates:
|
for update in updates:
|
||||||
for template_attr in self._template_attrs[update.template]:
|
for template_attr in self._template_attrs[update.template]:
|
||||||
template_attr.handle_result(
|
template_attr.handle_result(
|
||||||
event, update.template, update.last_result, update.result
|
event, update.template, update.last_result, update.result
|
||||||
)
|
)
|
||||||
|
if isinstance(update.result, TemplateError):
|
||||||
|
errors.append(update.result)
|
||||||
|
|
||||||
if not self._preview_callback:
|
if not self._preview_callback:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
self._preview_callback(None, None, None, str(errors[-1]))
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
calculated_state = self._async_calculate_state()
|
calculated_state = self._async_calculate_state()
|
||||||
validate_state(calculated_state.state)
|
validate_state(calculated_state.state)
|
||||||
@@ -451,13 +458,19 @@ class TemplateEntity(AbstractTemplateEntity):
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Render a preview."""
|
"""Render a preview."""
|
||||||
|
|
||||||
def log_template_error(level: int, msg: str) -> None:
|
def suppress_preview_errors(level: int, msg: str) -> None:
|
||||||
preview_callback(None, None, None, msg)
|
"""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._preview_callback = preview_callback
|
||||||
self._async_setup_templates()
|
self._async_setup_templates()
|
||||||
try:
|
try:
|
||||||
self._async_template_startup(None, log_template_error)
|
self._async_template_startup(None, suppress_preview_errors)
|
||||||
except Exception as err: # noqa: BLE001
|
except Exception as err: # noqa: BLE001
|
||||||
preview_callback(None, None, None, str(err))
|
preview_callback(None, None, None, str(err))
|
||||||
return self._call_on_remove_callbacks
|
return self._call_on_remove_callbacks
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ from pytest_unordered import unordered
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.template import DOMAIN, async_setup_entry
|
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.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
@@ -872,10 +873,10 @@ async def test_options(
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"sensor",
|
"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"},
|
{"one": "30.0", "two": "20.0"},
|
||||||
["", STATE_UNKNOWN, "50.0"],
|
["0.0", "30.0", "50.0"],
|
||||||
[{}, {}],
|
[{}, {}],
|
||||||
[["one", "two"], ["one", "two"]],
|
[["one", "two"], ["one", "two"]],
|
||||||
),
|
),
|
||||||
@@ -1124,7 +1125,7 @@ async def test_config_flow_preview_bad_input(
|
|||||||
"sensor",
|
"sensor",
|
||||||
"{{ float(states('sensor.one')) + float(states('sensor.two')) }}",
|
"{{ float(states('sensor.one')) + float(states('sensor.two')) }}",
|
||||||
{"one": "30.0", "two": "20.0"},
|
{"one": "30.0", "two": "20.0"},
|
||||||
["unavailable", "50.0"],
|
["50.0"],
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
"ValueError: Template error: float got invalid input 'unknown' "
|
"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["type"] == "event"
|
||||||
assert msg["event"] == {"error": error_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:
|
for input_entity in input_entities:
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
f"{template_type}.{input_entity}", input_states[input_entity], {}
|
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()
|
msg = await client.receive_json()
|
||||||
assert msg["type"] == "event"
|
assert msg["type"] == "event"
|
||||||
assert msg["event"]["state"] == template_states[1]
|
assert msg["event"]["state"] == template_states[0]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -1208,8 +1205,8 @@ async def test_config_flow_preview_template_startup_error(
|
|||||||
"sensor",
|
"sensor",
|
||||||
"{{ float(states('sensor.one')) > 30 and undefined_function() }}",
|
"{{ float(states('sensor.one')) > 30 and undefined_function() }}",
|
||||||
[{"one": "30.0", "two": "20.0"}, {"one": "35.0", "two": "20.0"}],
|
[{"one": "30.0", "two": "20.0"}, {"one": "35.0", "two": "20.0"}],
|
||||||
["False", "unavailable"],
|
["False"],
|
||||||
["'undefined_function' is undefined"],
|
["UndefinedError: 'undefined_function' is undefined"],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -1273,10 +1270,6 @@ async def test_config_flow_preview_template_error(
|
|||||||
assert msg["type"] == "event"
|
assert msg["type"] == "event"
|
||||||
assert msg["event"] == {"error": error_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(
|
@pytest.mark.parametrize(
|
||||||
(
|
(
|
||||||
@@ -1749,3 +1742,89 @@ async def test_options_flow_change_device(
|
|||||||
**state_template,
|
**state_template,
|
||||||
**extra_options,
|
**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)
|
||||||
|
|||||||
Reference in New Issue
Block a user