Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion homeassistant/components/websocket_api/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -267,7 +269,11 @@ def _template_listener(event, updates):

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
Expand Down
9 changes: 9 additions & 0 deletions homeassistant/helpers/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,15 @@ def async_setup(self) -> None:
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:
Expand Down
25 changes: 20 additions & 5 deletions tests/components/websocket_api/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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
Expand Down
87 changes: 82 additions & 5 deletions tests/helpers/test_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,39 +682,63 @@ def specific_run_callback(event, updates):
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()
assert len(specific_runs) == 3
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()
assert len(specific_runs) == 5
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()
Expand All @@ -723,26 +747,51 @@ def specific_run_callback(event, updates):
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):
Expand All @@ -766,14 +815,15 @@ def specific_run_callback(event, updates):
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()

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]
Expand Down Expand Up @@ -808,11 +858,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
Expand Down Expand Up @@ -851,10 +912,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()
Expand All @@ -864,11 +926,21 @@ def specific_run_callback(event, updates):
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()
Expand Down Expand Up @@ -924,7 +996,7 @@ def iterator_callback(event, updates):
def filter_callback(event, updates):
filter_runs.append(updates.pop().result)

async_track_template_result(
info = async_track_template_result(
hass,
[
TrackTemplate(
Expand All @@ -939,6 +1011,11 @@ def filter_callback(event, updates):
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()
Expand Down