mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Add limited template to at field for time triggers (#126584)
* Add limited template to at field for time triggers * fix mypy * Fix comments * fix-tests --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
1254667b2c
commit
810bf06e16
@ -3,7 +3,7 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import NamedTuple
|
from typing import Any, NamedTuple
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -26,7 +26,8 @@ from homeassistant.core import (
|
|||||||
State,
|
State,
|
||||||
callback,
|
callback,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import config_validation as cv, template
|
||||||
from homeassistant.helpers.event import (
|
from homeassistant.helpers.event import (
|
||||||
async_track_point_in_time,
|
async_track_point_in_time,
|
||||||
async_track_state_change_event,
|
async_track_state_change_event,
|
||||||
@ -37,6 +38,7 @@ from homeassistant.helpers.typing import ConfigType
|
|||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
_TIME_TRIGGER_ENTITY = vol.All(str, cv.entity_domain(["input_datetime", "sensor"]))
|
_TIME_TRIGGER_ENTITY = vol.All(str, cv.entity_domain(["input_datetime", "sensor"]))
|
||||||
|
_TIME_AT_SCHEMA = vol.Any(cv.time, _TIME_TRIGGER_ENTITY)
|
||||||
|
|
||||||
_TIME_TRIGGER_ENTITY_WITH_OFFSET = vol.Schema(
|
_TIME_TRIGGER_ENTITY_WITH_OFFSET = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -45,16 +47,29 @@ _TIME_TRIGGER_ENTITY_WITH_OFFSET = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def valid_at_template(value: Any) -> template.Template:
|
||||||
|
"""Validate either a jinja2 template, valid time, or valid trigger entity."""
|
||||||
|
tpl = cv.template(value)
|
||||||
|
|
||||||
|
if tpl.is_static:
|
||||||
|
_TIME_AT_SCHEMA(value)
|
||||||
|
|
||||||
|
return tpl
|
||||||
|
|
||||||
|
|
||||||
_TIME_TRIGGER_SCHEMA = vol.Any(
|
_TIME_TRIGGER_SCHEMA = vol.Any(
|
||||||
cv.time,
|
cv.time,
|
||||||
_TIME_TRIGGER_ENTITY,
|
_TIME_TRIGGER_ENTITY,
|
||||||
_TIME_TRIGGER_ENTITY_WITH_OFFSET,
|
_TIME_TRIGGER_ENTITY_WITH_OFFSET,
|
||||||
|
valid_at_template,
|
||||||
msg=(
|
msg=(
|
||||||
"Expected HH:MM, HH:MM:SS, an Entity ID with domain 'input_datetime' or "
|
"Expected HH:MM, HH:MM:SS, an Entity ID with domain 'input_datetime' or "
|
||||||
"'sensor', or a combination of a timestamp sensor entity and an offset."
|
"'sensor', a combination of a timestamp sensor entity and an offset, or Limited Template"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
|
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_PLATFORM): "time",
|
vol.Required(CONF_PLATFORM): "time",
|
||||||
@ -78,6 +93,7 @@ async def async_attach_trigger(
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
trigger_data = trigger_info["trigger_data"]
|
trigger_data = trigger_info["trigger_data"]
|
||||||
|
variables = trigger_info["variables"] or {}
|
||||||
entities: dict[tuple[str, timedelta], CALLBACK_TYPE] = {}
|
entities: dict[tuple[str, timedelta], CALLBACK_TYPE] = {}
|
||||||
removes: list[CALLBACK_TYPE] = []
|
removes: list[CALLBACK_TYPE] = []
|
||||||
job = HassJob(action, f"time trigger {trigger_info}")
|
job = HassJob(action, f"time trigger {trigger_info}")
|
||||||
@ -202,6 +218,16 @@ async def async_attach_trigger(
|
|||||||
to_track: list[TrackEntity] = []
|
to_track: list[TrackEntity] = []
|
||||||
|
|
||||||
for at_time in config[CONF_AT]:
|
for at_time in config[CONF_AT]:
|
||||||
|
if isinstance(at_time, template.Template):
|
||||||
|
render = template.render_complex(at_time, variables, limited=True)
|
||||||
|
try:
|
||||||
|
at_time = _TIME_AT_SCHEMA(render)
|
||||||
|
except vol.Invalid as exc:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"Limited Template for 'at' rendered a unexpected value '{render}', expected HH:MM, "
|
||||||
|
f"HH:MM:SS or Entity ID with domain 'input_datetime' or 'sensor'"
|
||||||
|
) from exc
|
||||||
|
|
||||||
if isinstance(at_time, str):
|
if isinstance(at_time, str):
|
||||||
# entity
|
# entity
|
||||||
update_entity_trigger(at_time, new_state=hass.states.get(at_time))
|
update_entity_trigger(at_time, new_state=hass.states.get(at_time))
|
||||||
|
@ -159,7 +159,10 @@ async def test_if_fires_using_at_input_datetime(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("conf_at", "trigger_deltas"),
|
("conf_at", "trigger_deltas"),
|
||||||
[
|
[
|
||||||
(["5:00:00", "6:00:00"], [timedelta(0), timedelta(hours=1)]),
|
(
|
||||||
|
["5:00:00", "6:00:00", "{{ '7:00:00' }}"],
|
||||||
|
[timedelta(0), timedelta(hours=1), timedelta(hours=2)],
|
||||||
|
),
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
"5:00:05",
|
"5:00:05",
|
||||||
@ -435,10 +438,14 @@ async def test_untrack_time_change(hass: HomeAssistant) -> None:
|
|||||||
assert len(mock_track_time_change.mock_calls) == 3
|
assert len(mock_track_time_change.mock_calls) == 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("at_sensor"), ["sensor.next_alarm", "{{ 'sensor.next_alarm' }}"]
|
||||||
|
)
|
||||||
async def test_if_fires_using_at_sensor(
|
async def test_if_fires_using_at_sensor(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
freezer: FrozenDateTimeFactory,
|
freezer: FrozenDateTimeFactory,
|
||||||
service_calls: list[ServiceCall],
|
service_calls: list[ServiceCall],
|
||||||
|
at_sensor: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test for firing at sensor time."""
|
"""Test for firing at sensor time."""
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
@ -461,7 +468,7 @@ async def test_if_fires_using_at_sensor(
|
|||||||
automation.DOMAIN,
|
automation.DOMAIN,
|
||||||
{
|
{
|
||||||
automation.DOMAIN: {
|
automation.DOMAIN: {
|
||||||
"trigger": {"platform": "time", "at": "sensor.next_alarm"},
|
"trigger": {"platform": "time", "at": at_sensor},
|
||||||
"action": {
|
"action": {
|
||||||
"service": "test.automation",
|
"service": "test.automation",
|
||||||
"data_template": {"some": some_data},
|
"data_template": {"some": some_data},
|
||||||
@ -626,6 +633,9 @@ async def test_if_fires_using_at_sensor_with_offset(
|
|||||||
{"platform": "time", "at": "input_datetime.bla"},
|
{"platform": "time", "at": "input_datetime.bla"},
|
||||||
{"platform": "time", "at": "sensor.bla"},
|
{"platform": "time", "at": "sensor.bla"},
|
||||||
{"platform": "time", "at": "12:34"},
|
{"platform": "time", "at": "12:34"},
|
||||||
|
{"platform": "time", "at": "{{ '12:34' }}"},
|
||||||
|
{"platform": "time", "at": "{{ 'input_datetime.bla' }}"},
|
||||||
|
{"platform": "time", "at": "{{ 'sensor.bla' }}"},
|
||||||
{"platform": "time", "at": {"entity_id": "sensor.bla", "offset": "-00:01"}},
|
{"platform": "time", "at": {"entity_id": "sensor.bla", "offset": "-00:01"}},
|
||||||
{
|
{
|
||||||
"platform": "time",
|
"platform": "time",
|
||||||
@ -724,3 +734,70 @@ async def test_datetime_in_past_on_load(
|
|||||||
service_calls[2].data["some"]
|
service_calls[2].data["some"]
|
||||||
== f"time-{future.day}-{future.hour}-input_datetime.my_trigger"
|
== f"time-{future.day}-{future.hour}-input_datetime.my_trigger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"trigger",
|
||||||
|
[
|
||||||
|
{"platform": "time", "at": "{{ 'hello world' }}"},
|
||||||
|
{"platform": "time", "at": "{{ 74 }}"},
|
||||||
|
{"platform": "time", "at": "{{ true }}"},
|
||||||
|
{"platform": "time", "at": "{{ 7.5465 }}"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_if_at_template_renders_bad_value(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
trigger: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
"""Test for invalid templates."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": trigger,
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
"expected HH:MM, HH:MM:SS or Entity ID with domain 'input_datetime' or 'sensor'"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"trigger",
|
||||||
|
[
|
||||||
|
{"platform": "time", "at": "{{ now().strftime('%H:%M') }}"},
|
||||||
|
{"platform": "time", "at": "{{ states('sensor.blah') | int(0) }}"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_if_at_template_limited_template(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
trigger: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
"""Test for invalid templates."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": trigger,
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "is not supported in limited templates" in caplog.text
|
||||||
|
Loading…
x
Reference in New Issue
Block a user