Send template render errors to template helper preview (#99716)

pull/99746/head
Erik Montnemery 2023-09-06 16:07:05 +02:00 committed by GitHub
parent e1ea53e72f
commit c9a6ea94a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 190 additions and 19 deletions

View File

@ -15,13 +15,11 @@ from homeassistant.const import (
CONF_ICON,
CONF_ICON_TEMPLATE,
CONF_NAME,
EVENT_HOMEASSISTANT_START,
STATE_UNKNOWN,
)
from homeassistant.core import (
CALLBACK_TYPE,
Context,
CoreState,
HomeAssistant,
State,
callback,
@ -38,6 +36,7 @@ from homeassistant.helpers.event import (
async_track_template_result,
)
from homeassistant.helpers.script import Script, _VarsType
from homeassistant.helpers.start import async_at_start
from homeassistant.helpers.template import (
Template,
TemplateStateFromEntityId,
@ -442,7 +441,11 @@ class TemplateEntity(Entity):
)
@callback
def _async_template_startup(self, *_: Any) -> None:
def _async_template_startup(
self,
_hass: HomeAssistant | None,
log_fn: Callable[[int, str], None] | None = None,
) -> None:
template_var_tups: list[TrackTemplate] = []
has_availability_template = False
@ -467,6 +470,7 @@ class TemplateEntity(Entity):
self.hass,
template_var_tups,
self._handle_results,
log_fn=log_fn,
has_super_template=has_availability_template,
)
self.async_on_remove(result_info.async_remove)
@ -515,10 +519,13 @@ class TemplateEntity(Entity):
) -> CALLBACK_TYPE:
"""Render a preview."""
def log_template_error(level: int, msg: str) -> None:
preview_callback(None, None, None, msg)
self._preview_callback = preview_callback
self._async_setup_templates()
try:
self._async_template_startup()
self._async_template_startup(None, log_template_error)
except Exception as err: # pylint: disable=broad-exception-caught
preview_callback(None, None, None, str(err))
return self._call_on_remove_callbacks
@ -527,13 +534,7 @@ class TemplateEntity(Entity):
"""Run when entity about to be added to hass."""
self._async_setup_templates()
if self.hass.state == CoreState.running:
self._async_template_startup()
return
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, self._async_template_startup
)
async_at_start(self.hass, self._async_template_startup)
async def async_update(self) -> None:
"""Call for forced update."""

View File

@ -957,11 +957,14 @@ class TrackTemplateResultInfo:
if info.exception:
if raise_on_template_error:
raise info.exception
_LOGGER.error(
"Error while processing template: %s",
track_template_.template,
exc_info=info.exception,
)
if not log_fn:
_LOGGER.error(
"Error while processing template: %s",
track_template_.template,
exc_info=info.exception,
)
else:
log_fn(logging.ERROR, str(info.exception))
self._track_state_changes = async_track_state_change_filtered(
self.hass, _render_infos_to_track_states(self._info.values()), self._refresh

View File

@ -272,12 +272,12 @@ async def test_options(
),
(
"sensor",
"{{ float(states('sensor.one')) + float(states('sensor.two')) }}",
"{{ float(states('sensor.one'), default='') + float(states('sensor.two'), default='') }}",
{},
{"one": "30.0", "two": "20.0"},
["unavailable", "50.0"],
["", "50.0"],
[{}, {}],
[["one"], ["one", "two"]],
[["one", "two"], ["one", "two"]],
),
),
)
@ -470,6 +470,173 @@ async def test_config_flow_preview_bad_input(
}
@pytest.mark.parametrize(
(
"template_type",
"state_template",
"input_states",
"template_states",
"error_events",
),
[
(
"sensor",
"{{ float(states('sensor.one')) + float(states('sensor.two')) }}",
{"one": "30.0", "two": "20.0"},
["unavailable", "50.0"],
[
(
"ValueError: Template error: float got invalid input 'unknown' "
"when rendering template '{{ float(states('sensor.one')) + "
"float(states('sensor.two')) }}' but no default was specified"
)
],
),
],
)
async def test_config_flow_preview_template_startup_error(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
template_type: str,
state_template: str,
input_states: dict[str, str],
template_states: list[str],
error_events: list[str],
) -> None:
"""Test the config flow preview."""
client = await hass_ws_client(hass)
input_entities = ["one", "two"]
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.MENU
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"next_step_id": template_type},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == template_type
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": {"name": "My template", "state": state_template},
}
)
msg = await client.receive_json()
assert msg["type"] == "result"
assert msg["success"]
for error_event in error_events:
msg = await client.receive_json()
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], {}
)
msg = await client.receive_json()
assert msg["type"] == "event"
assert msg["event"]["state"] == template_states[1]
@pytest.mark.parametrize(
(
"template_type",
"state_template",
"input_states",
"template_states",
"error_events",
),
[
(
"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"],
),
],
)
async def test_config_flow_preview_template_error(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
template_type: str,
state_template: str,
input_states: list[dict[str, str]],
template_states: list[str],
error_events: list[str],
) -> None:
"""Test the config flow preview."""
client = await hass_ws_client(hass)
input_entities = ["one", "two"]
for input_entity in input_entities:
hass.states.async_set(
f"{template_type}.{input_entity}", input_states[0][input_entity], {}
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.MENU
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"next_step_id": template_type},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == template_type
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": {"name": "My template", "state": state_template},
}
)
msg = await client.receive_json()
assert msg["type"] == "result"
assert msg["success"]
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[1][input_entity], {}
)
for error_event in error_events:
msg = await client.receive_json()
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(
(
"template_type",