diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 8995f075f32..d80c7934dd4 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -17,6 +17,7 @@ from homeassistant.exceptions import ( from homeassistant.helpers import config_validation as cv, entity from homeassistant.helpers.event import TrackTemplate, async_track_template_result from homeassistant.helpers.service import async_get_all_descriptions +from homeassistant.helpers.template import Template from homeassistant.loader import IntegrationNotFound, async_get_integration from . import const, decorators, messages @@ -242,16 +243,15 @@ def handle_ping(hass, connection, msg): @decorators.websocket_command( { vol.Required("type"): "render_template", - vol.Required("template"): cv.template, + vol.Required("template"): str, vol.Optional("entity_ids"): cv.entity_ids, vol.Optional("variables"): dict, } ) def handle_render_template(hass, connection, msg): """Handle render_template command.""" - template = msg["template"] - template.hass = hass - + template_str = msg["template"] + template = Template(template_str, hass) variables = msg.get("variables") info = None @@ -261,13 +261,8 @@ def handle_render_template(hass, connection, msg): track_template_result = updates.pop() result = track_template_result.result if isinstance(result, TemplateError): - _LOGGER.error( - "TemplateError('%s') " "while processing template '%s'", - result, - track_template_result.template, - ) - - result = None + connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(result)) + return connection.send_message( messages.event_message( @@ -275,9 +270,16 @@ def handle_render_template(hass, connection, msg): ) ) - info = async_track_template_result( - hass, [TrackTemplate(template, variables)], _template_listener - ) + try: + info = async_track_template_result( + hass, + [TrackTemplate(template, variables)], + _template_listener, + raise_on_template_error=True, + ) + except TemplateError as ex: + connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) + return connection.subscriptions[msg["id"]] = info.async_remove diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index f01a2880b9d..5f2cfb2257d 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -29,6 +29,7 @@ ERR_UNKNOWN_COMMAND = "unknown_command" ERR_UNKNOWN_ERROR = "unknown_error" ERR_UNAUTHORIZED = "unauthorized" ERR_TIMEOUT = "timeout" +ERR_TEMPLATE_ERROR = "template_error" TYPE_RESULT = "result" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 5ae1a2bf23a..d7b1f171a51 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -565,7 +565,7 @@ class _TrackTemplateResultInfo: self._last_domains: Set = set() self._last_entities: Set = set() - def async_setup(self) -> None: + def async_setup(self, raise_on_template_error: bool) -> None: """Activation of template tracking.""" for track_template_ in self._track_templates: template = track_template_.template @@ -573,6 +573,8 @@ class _TrackTemplateResultInfo: self._info[template] = template.async_render_to_info(variables) if self._info[template].exception: + if raise_on_template_error: + raise self._info[template].exception _LOGGER.error( "Error while processing template: %s", track_template_.template, @@ -812,6 +814,7 @@ def async_track_template_result( hass: HomeAssistant, track_templates: Iterable[TrackTemplate], action: TrackTemplateResultListener, + raise_on_template_error: bool = False, ) -> _TrackTemplateResultInfo: """Add a listener that fires when a the result of a template changes. @@ -833,9 +836,13 @@ def async_track_template_result( Home assistant object. track_templates An iterable of TrackTemplate. - action Callable to call with results. + raise_on_template_error + When set to True, if there is an exception + processing the template during setup, the system + will raise the exception instead of setting up + tracking. Returns ------- @@ -843,7 +850,7 @@ def async_track_template_result( """ tracker = _TrackTemplateResultInfo(hass, track_templates, action) - tracker.async_setup() + tracker.async_setup(raise_on_template_error) return tracker diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index f5d8bc6a6ae..5564024a92b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -266,7 +266,7 @@ class Template: try: self._compiled_code = self._env.compile(self.template) - except jinja2.exceptions.TemplateSyntaxError as err: + except jinja2.TemplateError as err: raise TemplateError(err) from err def extract_entities( diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 1b9eea86018..ea6f2f42bdc 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -484,21 +484,59 @@ async def test_render_template_with_error( ) msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + + assert "TemplateError" not in caplog.text + + +async def test_render_template_with_delayed_error( + hass, websocket_client, hass_admin_user, caplog +): + """Test a template with an error that only happens after a state change.""" + hass.states.async_set("sensor.test", "on") + await hass.async_block_till_done() + + template_str = """ +{% if states.sensor.test.state %} + on +{% else %} + {{ explode + 1 }} +{% endif %} + """ + + await websocket_client.send_json( + {"id": 5, "type": "render_template", "template": template_str} + ) + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 assert msg["type"] == const.TYPE_RESULT assert msg["success"] + hass.states.async_remove("sensor.test") + await hass.async_block_till_done() + msg = await websocket_client.receive_json() assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] assert event == { - "result": None, - "listeners": {"all": True, "domains": [], "entities": []}, + "result": "on", + "listeners": {"all": False, "domains": [], "entities": ["sensor.test"]}, } - assert "my_unknown_var" in caplog.text - assert "TemplateError" in caplog.text + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + + assert "TemplateError" not in caplog.text async def test_render_template_returns_with_match_all( diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 479984b97f1..8bdf9cb891c 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1460,6 +1460,25 @@ async def test_async_track_template_result_multiple_templates_mixing_domain(hass ] +async def test_async_track_template_result_raise_on_template_error(hass): + """Test that we raise as soon as we encounter a failed template.""" + + with pytest.raises(TemplateError): + async_track_template_result( + hass, + [ + TrackTemplate( + Template( + "{{ states.switch | function_that_does_not_exist | list }}" + ), + None, + ), + ], + ha.callback(lambda event, updates: None), + raise_on_template_error=True, + ) + + async def test_track_same_state_simple_no_trigger(hass): """Test track_same_change with no trigger.""" callback_runs = []