From e7e083c547fdd7a125dbcc14b92376bbeef3be7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Sat, 10 Aug 2019 21:46:49 +0200 Subject: [PATCH] Websocket call for rendering jinja2 templates subscription (#25614) * Websocket call for rendering jinja2 templates * Address review comments * Address review comments * Allow MATCH_ALL, but ignore it. * Always register unsub method. --- .../components/websocket_api/commands.py | 44 ++++++++++ .../components/websocket_api/test_commands.py | 82 +++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 0dc07ebfd3f..deb3600574f 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -7,6 +7,7 @@ from homeassistant.core import callback, DOMAIN as HASS_DOMAIN from homeassistant.exceptions import Unauthorized, ServiceNotFound, HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_get_all_descriptions +from homeassistant.helpers.event import async_track_state_change from . import const, decorators, messages @@ -21,6 +22,7 @@ def async_register_commands(hass, async_reg): async_reg(hass, handle_get_services) async_reg(hass, handle_get_config) async_reg(hass, handle_ping) + async_reg(hass, handle_render_template) def pong_message(iden): @@ -202,3 +204,45 @@ def handle_ping(hass, connection, msg): Async friendly. """ connection.send_message(pong_message(msg["id"])) + + +@callback +@decorators.websocket_command( + { + vol.Required("type"): "render_template", + vol.Required("template"): cv.template, + vol.Optional("entity_ids"): cv.entity_ids, + vol.Optional("variables"): dict, + } +) +def handle_render_template(hass, connection, msg): + """Handle render_template command. + + Async friendly. + """ + template = msg["template"] + template.hass = hass + + variables = msg.get("variables") + + entity_ids = msg.get("entity_ids") + if entity_ids is None: + entity_ids = template.extract_entities(variables) + + @callback + def state_listener(*_): + connection.send_message( + messages.event_message( + msg["id"], {"result": template.async_render(variables)} + ) + ) + + if entity_ids and entity_ids != MATCH_ALL: + connection.subscriptions[msg["id"]] = async_track_state_change( + hass, entity_ids, state_listener + ) + else: + connection.subscriptions[msg["id"]] = lambda: None + + connection.send_result(msg["id"]) + state_listener() diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 506a45694c0..a39a0a0e7a6 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -389,3 +389,85 @@ async def test_subscribe_unsubscribe_events_state_changed( assert msg["type"] == "event" assert msg["event"]["event_type"] == "state_changed" assert msg["event"]["data"]["entity_id"] == "light.permitted" + + +async def test_render_template_renders_template( + hass, websocket_client, hass_admin_user +): + """Test simple template is rendered and updated.""" + hass.states.async_set("light.test", "on") + + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": "State is: {{ states('light.test') }}", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: on"} + + hass.states.async_set("light.test", "off") + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: off"} + + +async def test_render_template_with_manual_entity_ids( + hass, websocket_client, hass_admin_user +): + """Test that updates to specified entity ids cause a template rerender.""" + hass.states.async_set("light.test", "on") + hass.states.async_set("light.test2", "on") + + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": "State is: {{ states('light.test') }}", + "entity_ids": ["light.test2"], + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: on"} + + hass.states.async_set("light.test2", "off") + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: on"} + + +async def test_render_template_returns_with_match_all( + hass, websocket_client, hass_admin_user +): + """Test that a template that would match with all entities still return success.""" + await websocket_client.send_json( + {"id": 5, "type": "render_template", "template": "State is: {{ 42 }}"} + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"]