Skip to content

Commit

Permalink
Send template render errors to template helper preview (#99716)
Browse files Browse the repository at this point in the history
  • Loading branch information
emontnemery authored and bramkragten committed Sep 6, 2023
1 parent 107ca83 commit 067f946
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 19 deletions.
23 changes: 12 additions & 11 deletions homeassistant/components/template/template_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@
CONF_ICON,
CONF_ICON_TEMPLATE,
CONF_NAME,
EVENT_HOMEASSISTANT_START,
STATE_UNKNOWN,
)
from homeassistant.core import (
CALLBACK_TYPE,
Context,
CoreState,
HomeAssistant,
State,
callback,
Expand All @@ -38,6 +36,7 @@
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,
Expand Down Expand Up @@ -442,7 +441,11 @@ def _handle_results(
)

@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

Expand All @@ -467,6 +470,7 @@ def _async_template_startup(self, *_: Any) -> None:
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)
Expand Down Expand Up @@ -515,10 +519,13 @@ def async_start_preview(
) -> 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
Expand All @@ -527,13 +534,7 @@ async def async_added_to_hass(self) -> None:
"""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."""
Expand Down
13 changes: 8 additions & 5 deletions homeassistant/helpers/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,11 +957,14 @@ def async_setup(
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
Expand Down
173 changes: 170 additions & 3 deletions tests/components/template/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]],
),
),
)
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 067f946

Please sign in to comment.