diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 04ad0ae3d3a..036cd690da2 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -253,9 +253,11 @@ def handle_render_template(hass, connection, msg): template.hass = hass variables = msg.get("variables") + info = None @callback def _template_listener(event, updates): + nonlocal info track_template_result = updates.pop() result = track_template_result.result if isinstance(result, TemplateError): @@ -267,7 +269,11 @@ def handle_render_template(hass, connection, msg): result = None - connection.send_message(messages.event_message(msg["id"], {"result": result})) + connection.send_message( + messages.event_message( + msg["id"], {"result": result, "listeners": info.listeners} # type: ignore + ) + ) info = async_track_template_result( hass, [TrackTemplate(template, variables)], _template_listener diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 4f30d255aec..8e126c7c14c 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -581,6 +581,15 @@ class _TrackTemplateResultInfo: self._last_info = self._info.copy() self._create_listeners() + @property + def listeners(self) -> Dict: + """State changes that will cause a re-render.""" + return { + "all": self._all_listener is not None, + "entities": self._last_entities, + "domains": self._last_domains, + } + @property def _needs_all_listener(self) -> bool: for track_template_ in self._track_templates: diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 4113a833872..1b9eea86018 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -420,14 +420,20 @@ async def test_render_template_renders_template( assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": "State is: on"} + assert event == { + "result": "State is: on", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } 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"} + assert event == { + "result": "State is: off", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } async def test_render_template_manual_entity_ids_no_longer_needed( @@ -453,14 +459,20 @@ async def test_render_template_manual_entity_ids_no_longer_needed( assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": "State is: on"} + assert event == { + "result": "State is: on", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } 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"} + assert event == { + "result": "State is: off", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } async def test_render_template_with_error( @@ -480,7 +492,10 @@ async def test_render_template_with_error( assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": None} + assert event == { + "result": None, + "listeners": {"all": True, "domains": [], "entities": []}, + } assert "my_unknown_var" in caplog.text assert "TemplateError" in caplog.text diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index fcb8655804e..821285cfbe1 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -809,20 +809,33 @@ async def test_track_template_result_complex(hass): hass.states.async_set("light.one", "on") hass.states.async_set("lock.one", "locked") - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template_complex, None)], specific_run_callback ) await hass.async_block_till_done() + assert info.listeners == {"all": True, "domains": set(), "entities": set()} + hass.states.async_set("sensor.domain", "light") await hass.async_block_till_done() assert len(specific_runs) == 1 assert specific_runs[0].strip() == "['light.one']" + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } + hass.states.async_set("sensor.domain", "lock") await hass.async_block_till_done() assert len(specific_runs) == 2 assert specific_runs[1].strip() == "['lock.one']" + assert info.listeners == { + "all": False, + "domains": {"lock"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("sensor.domain", "all") await hass.async_block_till_done() @@ -830,11 +843,17 @@ async def test_track_template_result_complex(hass): assert "light.one" in specific_runs[2] assert "lock.one" in specific_runs[2] assert "sensor.domain" in specific_runs[2] + assert info.listeners == {"all": True, "domains": set(), "entities": set()} hass.states.async_set("sensor.domain", "light") await hass.async_block_till_done() assert len(specific_runs) == 4 assert specific_runs[3].strip() == "['light.one']" + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("light.two", "on") await hass.async_block_till_done() @@ -842,6 +861,11 @@ async def test_track_template_result_complex(hass): assert "light.one" in specific_runs[4] assert "light.two" in specific_runs[4] assert "sensor.domain" not in specific_runs[4] + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("light.three", "on") await hass.async_block_till_done() @@ -850,26 +874,51 @@ async def test_track_template_result_complex(hass): assert "light.two" in specific_runs[5] assert "light.three" in specific_runs[5] assert "sensor.domain" not in specific_runs[5] + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("sensor.domain", "lock") await hass.async_block_till_done() assert len(specific_runs) == 7 assert specific_runs[6].strip() == "['lock.one']" + assert info.listeners == { + "all": False, + "domains": {"lock"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("sensor.domain", "single_binary_sensor") await hass.async_block_till_done() assert len(specific_runs) == 8 assert specific_runs[7].strip() == "unknown" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"binary_sensor.single", "sensor.domain"}, + } hass.states.async_set("binary_sensor.single", "binary_sensor_on") await hass.async_block_till_done() assert len(specific_runs) == 9 assert specific_runs[8].strip() == "binary_sensor_on" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"binary_sensor.single", "sensor.domain"}, + } hass.states.async_set("sensor.domain", "lock") await hass.async_block_till_done() assert len(specific_runs) == 10 assert specific_runs[9].strip() == "['lock.one']" + assert info.listeners == { + "all": False, + "domains": {"lock"}, + "entities": {"sensor.domain"}, + } async def test_track_template_result_with_wildcard(hass): @@ -893,7 +942,7 @@ async def test_track_template_result_with_wildcard(hass): hass.states.async_set("cover.office_window", "closed") hass.states.async_set("cover.office_skylight", "open") - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template_complex, None)], specific_run_callback ) await hass.async_block_till_done() @@ -901,6 +950,7 @@ async def test_track_template_result_with_wildcard(hass): hass.states.async_set("cover.office_window", "open") await hass.async_block_till_done() assert len(specific_runs) == 1 + assert info.listeners == {"all": True, "domains": set(), "entities": set()} assert "cover.office_drapes=closed" in specific_runs[0] assert "cover.office_window=open" in specific_runs[0] @@ -935,11 +985,22 @@ async def test_track_template_result_with_group(hass): def specific_run_callback(event, updates): specific_runs.append(updates.pop().result) - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template_complex, None)], specific_run_callback ) await hass.async_block_till_done() + assert info.listeners == { + "all": False, + "domains": set(), + "entities": { + "group.power_sensors", + "sensor.power_1", + "sensor.power_2", + "sensor.power_3", + }, + } + hass.states.async_set("sensor.power_1", 100.1) await hass.async_block_till_done() assert len(specific_runs) == 1 @@ -978,10 +1039,11 @@ async def test_track_template_result_and_conditional(hass): def specific_run_callback(event, updates): specific_runs.append(updates.pop().result) - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template, None)], specific_run_callback ) await hass.async_block_till_done() + assert info.listeners == {"all": False, "domains": set(), "entities": {"light.a"}} hass.states.async_set("light.b", "on") await hass.async_block_till_done() @@ -991,11 +1053,21 @@ async def test_track_template_result_and_conditional(hass): await hass.async_block_till_done() assert len(specific_runs) == 1 assert specific_runs[0] == "on" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"light.a", "light.b"}, + } hass.states.async_set("light.b", "off") await hass.async_block_till_done() assert len(specific_runs) == 2 assert specific_runs[1] == "off" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"light.a", "light.b"}, + } hass.states.async_set("light.a", "off") await hass.async_block_till_done() @@ -1051,7 +1123,7 @@ async def test_track_template_result_iterator(hass): def filter_callback(event, updates): filter_runs.append(updates.pop().result) - async_track_template_result( + info = async_track_template_result( hass, [ TrackTemplate( @@ -1066,6 +1138,11 @@ async def test_track_template_result_iterator(hass): filter_callback, ) await hass.async_block_till_done() + assert info.listeners == { + "all": False, + "domains": {"sensor"}, + "entities": {"sensor.test"}, + } hass.states.async_set("sensor.test", 6) await hass.async_block_till_done()