Implement time tracking in templates (#41147)

Co-authored-by: Anders Melchiorsen <amelchio@nogoto.net>
This commit is contained in:
J. Nick Koston 2020-10-19 04:02:43 -05:00 committed by GitHub
parent 8949eb2442
commit 31c21126a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 374 additions and 32 deletions

View file

@ -992,7 +992,12 @@ async def test_track_template_result_complex(hass):
)
await hass.async_block_till_done()
assert info.listeners == {"all": True, "domains": set(), "entities": set()}
assert info.listeners == {
"all": True,
"domains": set(),
"entities": set(),
"time": False,
}
hass.states.async_set("sensor.domain", "light")
await hass.async_block_till_done()
@ -1003,6 +1008,7 @@ async def test_track_template_result_complex(hass):
"all": False,
"domains": {"light"},
"entities": {"sensor.domain"},
"time": False,
}
hass.states.async_set("sensor.domain", "lock")
@ -1013,6 +1019,7 @@ async def test_track_template_result_complex(hass):
"all": False,
"domains": {"lock"},
"entities": {"sensor.domain"},
"time": False,
}
hass.states.async_set("sensor.domain", "all")
@ -1021,7 +1028,12 @@ 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()}
assert info.listeners == {
"all": True,
"domains": set(),
"entities": set(),
"time": False,
}
hass.states.async_set("sensor.domain", "light")
await hass.async_block_till_done()
@ -1031,6 +1043,7 @@ async def test_track_template_result_complex(hass):
"all": False,
"domains": {"light"},
"entities": {"sensor.domain"},
"time": False,
}
hass.states.async_set("light.two", "on")
@ -1043,6 +1056,7 @@ async def test_track_template_result_complex(hass):
"all": False,
"domains": {"light"},
"entities": {"sensor.domain"},
"time": False,
}
hass.states.async_set("light.three", "on")
@ -1056,6 +1070,7 @@ async def test_track_template_result_complex(hass):
"all": False,
"domains": {"light"},
"entities": {"sensor.domain"},
"time": False,
}
hass.states.async_set("sensor.domain", "lock")
@ -1066,6 +1081,7 @@ async def test_track_template_result_complex(hass):
"all": False,
"domains": {"lock"},
"entities": {"sensor.domain"},
"time": False,
}
hass.states.async_set("sensor.domain", "single_binary_sensor")
@ -1076,6 +1092,7 @@ async def test_track_template_result_complex(hass):
"all": False,
"domains": set(),
"entities": {"binary_sensor.single", "sensor.domain"},
"time": False,
}
hass.states.async_set("binary_sensor.single", "binary_sensor_on")
@ -1086,6 +1103,7 @@ async def test_track_template_result_complex(hass):
"all": False,
"domains": set(),
"entities": {"binary_sensor.single", "sensor.domain"},
"time": False,
}
hass.states.async_set("sensor.domain", "lock")
@ -1096,6 +1114,7 @@ async def test_track_template_result_complex(hass):
"all": False,
"domains": {"lock"},
"entities": {"sensor.domain"},
"time": False,
}
@ -1128,7 +1147,12 @@ 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 info.listeners == {
"all": True,
"domains": set(),
"entities": set(),
"time": False,
}
assert "cover.office_drapes=closed" in specific_runs[0]
assert "cover.office_window=open" in specific_runs[0]
@ -1177,6 +1201,7 @@ async def test_track_template_result_with_group(hass):
"sensor.power_2",
"sensor.power_3",
},
"time": False,
}
hass.states.async_set("sensor.power_1", 100.1)
@ -1223,7 +1248,12 @@ async def test_track_template_result_and_conditional(hass):
hass, [TrackTemplate(template, None)], specific_run_callback
)
await hass.async_block_till_done()
assert info.listeners == {"all": False, "domains": set(), "entities": {"light.a"}}
assert info.listeners == {
"all": False,
"domains": set(),
"entities": {"light.a"},
"time": False,
}
hass.states.async_set("light.b", "on")
await hass.async_block_till_done()
@ -1237,6 +1267,7 @@ async def test_track_template_result_and_conditional(hass):
"all": False,
"domains": set(),
"entities": {"light.a", "light.b"},
"time": False,
}
hass.states.async_set("light.b", "off")
@ -1247,6 +1278,7 @@ async def test_track_template_result_and_conditional(hass):
"all": False,
"domains": set(),
"entities": {"light.a", "light.b"},
"time": False,
}
hass.states.async_set("light.a", "off")
@ -1324,6 +1356,7 @@ async def test_track_template_result_iterator(hass):
"all": False,
"domains": {"sensor"},
"entities": set(),
"time": False,
}
hass.states.async_set("sensor.test", 6)
@ -1664,6 +1697,7 @@ async def test_track_template_unavailable_sates_has_default_rate_limit(hass):
info.async_refresh()
await hass.async_block_till_done()
assert refresh_runs == [1, 2, 3]
info.async_remove()
async def test_specifically_referenced_entity_is_not_rate_limited(hass):
@ -1702,6 +1736,7 @@ async def test_specifically_referenced_entity_is_not_rate_limited(hass):
hass.states.async_set("sensor.one", "none")
await hass.async_block_till_done()
assert refresh_runs == ["1_none", "1_any", "3_none"]
info.async_remove()
async def test_track_two_templates_with_different_rate_limits(hass):
@ -1766,6 +1801,7 @@ async def test_track_two_templates_with_different_rate_limits(hass):
await hass.async_block_till_done()
assert refresh_runs[template_one] == [0, 1, 2]
assert refresh_runs[template_five] == [0, 1]
info.async_remove()
async def test_string(hass):
@ -1988,6 +2024,207 @@ async def test_async_track_template_result_raise_on_template_error(hass):
)
async def test_track_template_with_time(hass):
"""Test tracking template with time."""
hass.states.async_set("switch.test", "on")
specific_runs = []
template_complex = Template("{{ states.switch.test.state and now() }}", hass)
def specific_run_callback(event, updates):
specific_runs.append(updates.pop().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": {"switch.test"},
"time": True,
}
await hass.async_block_till_done()
now = dt_util.utcnow()
async_fire_time_changed(hass, now + timedelta(seconds=61))
async_fire_time_changed(hass, now + timedelta(seconds=61 * 2))
await hass.async_block_till_done()
assert specific_runs[-1] != specific_runs[0]
info.async_remove()
async def test_track_template_with_time_default(hass):
"""Test tracking template with time."""
specific_runs = []
template_complex = Template("{{ now() }}", hass)
def specific_run_callback(event, updates):
specific_runs.append(updates.pop().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": set(),
"time": True,
}
await hass.async_block_till_done()
now = dt_util.utcnow()
async_fire_time_changed(hass, now + timedelta(seconds=2))
async_fire_time_changed(hass, now + timedelta(seconds=4))
await hass.async_block_till_done()
assert len(specific_runs) < 2
async_fire_time_changed(hass, now + timedelta(minutes=2))
await hass.async_block_till_done()
async_fire_time_changed(hass, now + timedelta(minutes=4))
await hass.async_block_till_done()
assert len(specific_runs) >= 2
assert specific_runs[-1] != specific_runs[0]
info.async_remove()
async def test_track_template_with_time_that_leaves_scope(hass):
"""Test tracking template with time."""
hass.states.async_set("binary_sensor.washing_machine", "on")
specific_runs = []
template_complex = Template(
"""
{% if states.binary_sensor.washing_machine.state == "on" %}
{{ now() }}
{% else %}
{{ states.binary_sensor.washing_machine.last_updated }}
{% endif %}
""",
hass,
)
def specific_run_callback(event, updates):
specific_runs.append(updates.pop().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": {"binary_sensor.washing_machine"},
"time": True,
}
hass.states.async_set("binary_sensor.washing_machine", "off")
await hass.async_block_till_done()
assert info.listeners == {
"all": False,
"domains": set(),
"entities": {"binary_sensor.washing_machine"},
"time": False,
}
hass.states.async_set("binary_sensor.washing_machine", "on")
await hass.async_block_till_done()
assert info.listeners == {
"all": False,
"domains": set(),
"entities": {"binary_sensor.washing_machine"},
"time": True,
}
# Verify we do not update a second time
# if the state change happens
callback_count_before_time_change = len(specific_runs)
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=59))
await hass.async_block_till_done()
assert len(specific_runs) == callback_count_before_time_change
# Verify we do update on the next time change
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=61))
await hass.async_block_till_done()
assert len(specific_runs) == callback_count_before_time_change + 1
info.async_remove()
async def test_async_track_template_result_multiple_templates_mixing_listeners(hass):
"""Test tracking multiple templates with mixing listener types."""
template_1 = Template("{{ states.switch.test.state == 'on' }}")
template_2 = Template("{{ now() and True }}")
refresh_runs = []
@ha.callback
def refresh_listener(event, updates):
refresh_runs.append(updates)
now = dt_util.utcnow()
time_that_will_not_match_right_away = datetime(
now.year + 1, 5, 24, 11, 59, 55, tzinfo=dt_util.UTC
)
with patch(
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away
):
info = async_track_template_result(
hass,
[
TrackTemplate(template_1, None),
TrackTemplate(template_2, None),
],
refresh_listener,
)
assert info.listeners == {
"all": False,
"domains": set(),
"entities": {"switch.test"},
"time": True,
}
hass.states.async_set("switch.test", "on")
await hass.async_block_till_done()
assert refresh_runs == [
[
TrackTemplateResult(template_1, None, True),
]
]
refresh_runs = []
hass.states.async_set("switch.test", "off")
await hass.async_block_till_done()
assert refresh_runs == [
[
TrackTemplateResult(template_1, True, False),
]
]
refresh_runs = []
next_time = time_that_will_not_match_right_away + timedelta(hours=25)
with patch("homeassistant.util.dt.utcnow", return_value=next_time):
async_fire_time_changed(hass, next_time)
await hass.async_block_till_done()
assert refresh_runs == [
[
TrackTemplateResult(template_2, None, True),
]
]
async def test_track_same_state_simple_no_trigger(hass):
"""Test track_same_change with no trigger."""
callback_runs = []