Implement time tracking in templates (#41147)
Co-authored-by: Anders Melchiorsen <amelchio@nogoto.net>
This commit is contained in:
parent
8949eb2442
commit
31c21126a8
5 changed files with 374 additions and 32 deletions
|
@ -753,6 +753,7 @@ class _TrackTemplateResultInfo:
|
|||
self._rate_limit = KeyedRateLimit(hass)
|
||||
self._info: Dict[Template, RenderInfo] = {}
|
||||
self._track_state_changes: Optional[_TrackStateChangeFiltered] = None
|
||||
self._time_listeners: Dict[Template, Callable] = {}
|
||||
|
||||
def async_setup(self, raise_on_template_error: bool) -> None:
|
||||
"""Activation of template tracking."""
|
||||
|
@ -773,6 +774,7 @@ class _TrackTemplateResultInfo:
|
|||
self._track_state_changes = async_track_state_change_filtered(
|
||||
self.hass, _render_infos_to_track_states(self._info.values()), self._refresh
|
||||
)
|
||||
self._update_time_listeners()
|
||||
_LOGGER.debug(
|
||||
"Template group %s listens for %s",
|
||||
self._track_templates,
|
||||
|
@ -783,7 +785,38 @@ class _TrackTemplateResultInfo:
|
|||
def listeners(self) -> Dict:
|
||||
"""State changes that will cause a re-render."""
|
||||
assert self._track_state_changes
|
||||
return self._track_state_changes.listeners
|
||||
return {
|
||||
**self._track_state_changes.listeners,
|
||||
"time": bool(self._time_listeners),
|
||||
}
|
||||
|
||||
@callback
|
||||
def _setup_time_listener(self, template: Template, has_time: bool) -> None:
|
||||
if template in self._time_listeners:
|
||||
self._time_listeners.pop(template)()
|
||||
|
||||
# now() or utcnow() has left the scope of the template
|
||||
if not has_time:
|
||||
return
|
||||
|
||||
track_templates = [
|
||||
track_template_
|
||||
for track_template_ in self._track_templates
|
||||
if track_template_.template == template
|
||||
]
|
||||
|
||||
@callback
|
||||
def _refresh_from_time(now: datetime) -> None:
|
||||
self._refresh(None, track_templates=track_templates)
|
||||
|
||||
self._time_listeners[template] = async_call_later(
|
||||
self.hass, 60.45, _refresh_from_time
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_time_listeners(self) -> None:
|
||||
for template, info in self._info.items():
|
||||
self._setup_time_listener(template, info.has_time)
|
||||
|
||||
@callback
|
||||
def async_remove(self) -> None:
|
||||
|
@ -791,6 +824,8 @@ class _TrackTemplateResultInfo:
|
|||
assert self._track_state_changes
|
||||
self._track_state_changes.async_remove()
|
||||
self._rate_limit.async_remove()
|
||||
for template in list(self._time_listeners):
|
||||
self._time_listeners.pop(template)()
|
||||
|
||||
@callback
|
||||
def async_refresh(self) -> None:
|
||||
|
@ -889,7 +924,11 @@ class _TrackTemplateResultInfo:
|
|||
if not update:
|
||||
continue
|
||||
|
||||
template = track_template_.template
|
||||
self._setup_time_listener(template, self._info[template].has_time)
|
||||
|
||||
info_changed = True
|
||||
|
||||
if isinstance(update, TrackTemplateResult):
|
||||
updates.append(update)
|
||||
|
||||
|
|
|
@ -203,10 +203,11 @@ class RenderInfo:
|
|||
self.domains_lifecycle = set()
|
||||
self.entities = set()
|
||||
self.rate_limit = None
|
||||
self.has_time = False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Representation of RenderInfo."""
|
||||
return f"<RenderInfo {self.template} all_states={self.all_states} all_states_lifecycle={self.all_states_lifecycle} domains={self.domains} domains_lifecycle={self.domains_lifecycle} entities={self.entities} rate_limit={self.rate_limit}>"
|
||||
return f"<RenderInfo {self.template} all_states={self.all_states} all_states_lifecycle={self.all_states_lifecycle} domains={self.domains} domains_lifecycle={self.domains_lifecycle} entities={self.entities} rate_limit={self.rate_limit}> has_time={self.has_time}"
|
||||
|
||||
def _filter_domains_and_entities(self, entity_id: str) -> bool:
|
||||
"""Template should re-render if the entity state changes when we match specific domains or entities."""
|
||||
|
@ -961,6 +962,24 @@ def state_attr(hass, entity_id, name):
|
|||
return None
|
||||
|
||||
|
||||
def now(hass):
|
||||
"""Record fetching now."""
|
||||
render_info = hass.data.get(_RENDER_INFO)
|
||||
if render_info is not None:
|
||||
render_info.has_time = True
|
||||
|
||||
return dt_util.now()
|
||||
|
||||
|
||||
def utcnow(hass):
|
||||
"""Record fetching utcnow."""
|
||||
render_info = hass.data.get(_RENDER_INFO)
|
||||
if render_info is not None:
|
||||
render_info.has_time = True
|
||||
|
||||
return dt_util.utcnow()
|
||||
|
||||
|
||||
def forgiving_round(value, precision=0, method="common"):
|
||||
"""Round accepted strings."""
|
||||
try:
|
||||
|
@ -1291,9 +1310,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
|||
self.globals["atan"] = arc_tangent
|
||||
self.globals["atan2"] = arc_tangent2
|
||||
self.globals["float"] = forgiving_float
|
||||
self.globals["now"] = dt_util.now
|
||||
self.globals["as_local"] = dt_util.as_local
|
||||
self.globals["utcnow"] = dt_util.utcnow
|
||||
self.globals["as_timestamp"] = forgiving_as_timestamp
|
||||
self.globals["relative_time"] = relative_time
|
||||
self.globals["timedelta"] = timedelta
|
||||
|
@ -1324,6 +1341,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
|||
self.globals["is_state_attr"] = hassfunction(is_state_attr)
|
||||
self.globals["state_attr"] = hassfunction(state_attr)
|
||||
self.globals["states"] = AllStates(hass)
|
||||
self.globals["utcnow"] = hassfunction(utcnow)
|
||||
self.globals["now"] = hassfunction(now)
|
||||
|
||||
def is_safe_callable(self, obj):
|
||||
"""Test if callback is safe."""
|
||||
|
|
|
@ -417,7 +417,12 @@ async def test_render_template_renders_template(hass, websocket_client):
|
|||
event = msg["event"]
|
||||
assert event == {
|
||||
"result": "State is: on",
|
||||
"listeners": {"all": False, "domains": [], "entities": ["light.test"]},
|
||||
"listeners": {
|
||||
"all": False,
|
||||
"domains": [],
|
||||
"entities": ["light.test"],
|
||||
"time": False,
|
||||
},
|
||||
}
|
||||
|
||||
hass.states.async_set("light.test", "off")
|
||||
|
@ -427,7 +432,12 @@ async def test_render_template_renders_template(hass, websocket_client):
|
|||
event = msg["event"]
|
||||
assert event == {
|
||||
"result": "State is: off",
|
||||
"listeners": {"all": False, "domains": [], "entities": ["light.test"]},
|
||||
"listeners": {
|
||||
"all": False,
|
||||
"domains": [],
|
||||
"entities": ["light.test"],
|
||||
"time": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
@ -456,7 +466,12 @@ async def test_render_template_manual_entity_ids_no_longer_needed(
|
|||
event = msg["event"]
|
||||
assert event == {
|
||||
"result": "State is: on",
|
||||
"listeners": {"all": False, "domains": [], "entities": ["light.test"]},
|
||||
"listeners": {
|
||||
"all": False,
|
||||
"domains": [],
|
||||
"entities": ["light.test"],
|
||||
"time": False,
|
||||
},
|
||||
}
|
||||
|
||||
hass.states.async_set("light.test", "off")
|
||||
|
@ -466,7 +481,12 @@ async def test_render_template_manual_entity_ids_no_longer_needed(
|
|||
event = msg["event"]
|
||||
assert event == {
|
||||
"result": "State is: off",
|
||||
"listeners": {"all": False, "domains": [], "entities": ["light.test"]},
|
||||
"listeners": {
|
||||
"all": False,
|
||||
"domains": [],
|
||||
"entities": ["light.test"],
|
||||
"time": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
@ -553,7 +573,12 @@ async def test_render_template_with_delayed_error(hass, websocket_client, caplog
|
|||
event = msg["event"]
|
||||
assert event == {
|
||||
"result": "on",
|
||||
"listeners": {"all": False, "domains": [], "entities": ["sensor.test"]},
|
||||
"listeners": {
|
||||
"all": False,
|
||||
"domains": [],
|
||||
"entities": ["sensor.test"],
|
||||
"time": False,
|
||||
},
|
||||
}
|
||||
|
||||
msg = await websocket_client.receive_json()
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -856,10 +856,26 @@ def test_now(mock_is_safe, hass):
|
|||
"""Test now method."""
|
||||
now = dt_util.now()
|
||||
with patch("homeassistant.util.dt.now", return_value=now):
|
||||
assert (
|
||||
now.isoformat()
|
||||
== template.Template("{{ now().isoformat() }}", hass).async_render()
|
||||
info = template.Template("{{ now().isoformat() }}", hass).async_render_to_info()
|
||||
assert now.isoformat() == info.result()
|
||||
|
||||
assert info.has_time is True
|
||||
|
||||
|
||||
@patch(
|
||||
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
|
||||
return_value=True,
|
||||
)
|
||||
def test_utcnow(mock_is_safe, hass):
|
||||
"""Test now method."""
|
||||
utcnow = dt_util.utcnow()
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=utcnow):
|
||||
info = template.Template(
|
||||
"{{ utcnow().isoformat() }}", hass
|
||||
).async_render_to_info()
|
||||
assert utcnow.isoformat() == info.result()
|
||||
|
||||
assert info.has_time is True
|
||||
|
||||
|
||||
@patch(
|
||||
|
@ -966,20 +982,6 @@ def test_timedelta(mock_is_safe, hass):
|
|||
)
|
||||
|
||||
|
||||
@patch(
|
||||
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
|
||||
return_value=True,
|
||||
)
|
||||
def test_utcnow(mock_is_safe, hass):
|
||||
"""Test utcnow method."""
|
||||
now = dt_util.utcnow()
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||
assert (
|
||||
now.isoformat()
|
||||
== template.Template("{{ utcnow().isoformat() }}", hass).async_render()
|
||||
)
|
||||
|
||||
|
||||
def test_regex_match(hass):
|
||||
"""Test regex_match method."""
|
||||
tpl = template.Template(
|
||||
|
@ -1629,6 +1631,7 @@ async def test_async_render_to_info_with_wildcard_matching_state(hass):
|
|||
hass.states.async_set("cover.office_skylight", "open")
|
||||
hass.states.async_set("cover.x_skylight", "open")
|
||||
hass.states.async_set("binary_sensor.door", "open")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
info = render_to_info(hass, template_complex_str)
|
||||
|
||||
|
@ -2458,6 +2461,18 @@ async def test_lifecycle(hass):
|
|||
hass.states.async_set("sun.sun", "above", {"elevation": 50, "next_rising": "later"})
|
||||
for i in range(2):
|
||||
hass.states.async_set(f"sensor.sensor{i}", "on")
|
||||
hass.states.async_set("sensor.removed", "off")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set("sun.sun", "below", {"elevation": 60, "next_rising": "later"})
|
||||
for i in range(2):
|
||||
hass.states.async_set(f"sensor.sensor{i}", "off")
|
||||
|
||||
hass.states.async_set("sensor.new", "off")
|
||||
hass.states.async_remove("sensor.removed")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
tmp = template.Template("{{ states | count }}", hass)
|
||||
|
||||
|
@ -2465,6 +2480,8 @@ async def test_lifecycle(hass):
|
|||
assert info.all_states is False
|
||||
assert info.all_states_lifecycle is True
|
||||
assert info.rate_limit is None
|
||||
assert info.has_time is False
|
||||
|
||||
assert info.entities == set()
|
||||
assert info.domains == set()
|
||||
assert info.domains_lifecycle == set()
|
||||
|
@ -2539,9 +2556,15 @@ async def test_template_errors(hass):
|
|||
with pytest.raises(TemplateError):
|
||||
template.Template("{{ now() | rando }}", hass).async_render()
|
||||
|
||||
with pytest.raises(TemplateError):
|
||||
template.Template("{{ utcnow() | rando }}", hass).async_render()
|
||||
|
||||
with pytest.raises(TemplateError):
|
||||
template.Template("{{ now() | random }}", hass).async_render()
|
||||
|
||||
with pytest.raises(TemplateError):
|
||||
template.Template("{{ utcnow() | random }}", hass).async_render()
|
||||
|
||||
|
||||
async def test_state_attributes(hass):
|
||||
"""Test state attributes."""
|
||||
|
@ -2624,7 +2647,6 @@ async def test_legacy_templates(hass):
|
|||
)
|
||||
|
||||
await async_process_ha_core_config(hass, {"legacy_templates": True})
|
||||
|
||||
assert (
|
||||
template.Template("{{ states.sensor.temperature.state }}", hass).async_render()
|
||||
== "12"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue