mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Time trigger can also accept an input_datetime Entity ID (#38698)
This commit is contained in:
parent
dd86de3255
commit
192fe58fc8
@ -1,4 +1,5 @@
|
||||
"""Offer time listening automation rules."""
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@ -6,30 +7,99 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_AT, CONF_PLATFORM
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_change
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_point_in_time,
|
||||
async_track_state_change,
|
||||
async_track_time_change,
|
||||
)
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
# mypy: allow-untyped-defs, no-check-untyped-defs
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_TIME_TRIGGER_SCHEMA = vol.Any(
|
||||
cv.time,
|
||||
vol.All(str, cv.entity_domain("input_datetime")),
|
||||
msg="Expected HH:MM, HH:MM:SS or Entity ID from domain 'input_datetime'",
|
||||
)
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "time",
|
||||
vol.Required(CONF_AT): vol.All(cv.ensure_list, [cv.time]),
|
||||
vol.Required(CONF_AT): vol.All(cv.ensure_list, [_TIME_TRIGGER_SCHEMA]),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_attach_trigger(hass, config, action, automation_info):
|
||||
"""Listen for state changes based on configuration."""
|
||||
at_times = config[CONF_AT]
|
||||
entities = {}
|
||||
removes = []
|
||||
|
||||
@callback
|
||||
def time_automation_listener(now):
|
||||
"""Listen for time changes and calls action."""
|
||||
hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}})
|
||||
|
||||
removes = [
|
||||
@callback
|
||||
def update_entity_trigger(entity_id, old_state=None, new_state=None):
|
||||
# If a listener was already set up for entity, remove it.
|
||||
remove = entities.get(entity_id)
|
||||
if remove:
|
||||
remove()
|
||||
removes.remove(remove)
|
||||
remove = None
|
||||
|
||||
# Check state of entity. If valid, set up a listener.
|
||||
if new_state:
|
||||
has_date = new_state.attributes["has_date"]
|
||||
if has_date:
|
||||
year = new_state.attributes["year"]
|
||||
month = new_state.attributes["month"]
|
||||
day = new_state.attributes["day"]
|
||||
has_time = new_state.attributes["has_time"]
|
||||
if has_time:
|
||||
hour = new_state.attributes["hour"]
|
||||
minute = new_state.attributes["minute"]
|
||||
second = new_state.attributes["second"]
|
||||
else:
|
||||
# If no time then use midnight.
|
||||
hour = minute = second = 0
|
||||
|
||||
if has_date:
|
||||
# If input_datetime has date, then track point in time.
|
||||
trigger_dt = dt_util.DEFAULT_TIME_ZONE.localize(
|
||||
datetime(year, month, day, hour, minute, second)
|
||||
)
|
||||
# Only set up listener if time is now or in the future.
|
||||
if trigger_dt >= dt_util.now():
|
||||
remove = async_track_point_in_time(
|
||||
hass, time_automation_listener, trigger_dt
|
||||
)
|
||||
elif has_time:
|
||||
# Else if it has time, then track time change.
|
||||
remove = async_track_time_change(
|
||||
hass,
|
||||
time_automation_listener,
|
||||
hour=hour,
|
||||
minute=minute,
|
||||
second=second,
|
||||
)
|
||||
|
||||
# Was a listener set up?
|
||||
if remove:
|
||||
removes.append(remove)
|
||||
|
||||
entities[entity_id] = remove
|
||||
|
||||
for at_time in config[CONF_AT]:
|
||||
if isinstance(at_time, str):
|
||||
# input_datetime entity
|
||||
update_entity_trigger(at_time, new_state=hass.states.get(at_time))
|
||||
else:
|
||||
# datetime.time
|
||||
removes.append(
|
||||
async_track_time_change(
|
||||
hass,
|
||||
time_automation_listener,
|
||||
@ -37,8 +107,12 @@ async def async_attach_trigger(hass, config, action, automation_info):
|
||||
minute=at_time.minute,
|
||||
second=at_time.second,
|
||||
)
|
||||
for at_time in at_times
|
||||
]
|
||||
)
|
||||
|
||||
# Track state changes of any entities.
|
||||
removes.append(
|
||||
async_track_state_change(hass, list(entities), update_entity_trigger)
|
||||
)
|
||||
|
||||
@callback
|
||||
def remove_track_time_changes():
|
||||
|
@ -34,8 +34,8 @@ async def test_if_fires_using_at(hass, calls):
|
||||
now = dt_util.utcnow()
|
||||
|
||||
time_that_will_not_match_right_away = now.replace(
|
||||
year=now.year + 1, hour=4, minute=59, second=0
|
||||
)
|
||||
hour=4, minute=59, second=0
|
||||
) + timedelta(2)
|
||||
|
||||
with patch(
|
||||
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away
|
||||
@ -59,7 +59,7 @@ async def test_if_fires_using_at(hass, calls):
|
||||
now = dt_util.utcnow()
|
||||
|
||||
async_fire_time_changed(
|
||||
hass, now.replace(year=now.year + 1, hour=5, minute=0, second=0)
|
||||
hass, now.replace(hour=5, minute=0, second=0) + timedelta(2)
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
@ -67,14 +67,90 @@ async def test_if_fires_using_at(hass, calls):
|
||||
assert calls[0].data["some"] == "time - 5"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"has_date,has_time", [(True, True), (True, False), (False, True)]
|
||||
)
|
||||
async def test_if_fires_using_at_input_datetime(hass, calls, has_date, has_time):
|
||||
"""Test for firing at input_datetime."""
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"input_datetime",
|
||||
{"input_datetime": {"trigger": {"has_date": has_date, "has_time": has_time}}},
|
||||
)
|
||||
|
||||
now = dt_util.now()
|
||||
|
||||
trigger_dt = now.replace(
|
||||
hour=5 if has_time else 0, minute=0, second=0, microsecond=0
|
||||
) + timedelta(2)
|
||||
|
||||
await hass.services.async_call(
|
||||
"input_datetime",
|
||||
"set_datetime",
|
||||
{
|
||||
ATTR_ENTITY_ID: "input_datetime.trigger",
|
||||
"datetime": str(trigger_dt.replace(tzinfo=None)),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1)
|
||||
|
||||
some_data = "{{ trigger.platform }}-{{ trigger.now.day }}-{{ trigger.now.hour }}"
|
||||
with patch(
|
||||
"homeassistant.util.dt.utcnow",
|
||||
return_value=dt_util.as_utc(time_that_will_not_match_right_away),
|
||||
):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {"platform": "time", "at": "input_datetime.trigger"},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {"some": some_data},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async_fire_time_changed(hass, trigger_dt)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data["some"] == f"time-{trigger_dt.day}-{trigger_dt.hour}"
|
||||
|
||||
if has_date:
|
||||
trigger_dt += timedelta(days=1)
|
||||
if has_time:
|
||||
trigger_dt += timedelta(hours=1)
|
||||
|
||||
await hass.services.async_call(
|
||||
"input_datetime",
|
||||
"set_datetime",
|
||||
{
|
||||
ATTR_ENTITY_ID: "input_datetime.trigger",
|
||||
"datetime": str(trigger_dt.replace(tzinfo=None)),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
async_fire_time_changed(hass, trigger_dt)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 2
|
||||
assert calls[1].data["some"] == f"time-{trigger_dt.day}-{trigger_dt.hour}"
|
||||
|
||||
|
||||
async def test_if_fires_using_multiple_at(hass, calls):
|
||||
"""Test for firing at."""
|
||||
|
||||
now = dt_util.utcnow()
|
||||
|
||||
time_that_will_not_match_right_away = now.replace(
|
||||
year=now.year + 1, hour=4, minute=59, second=0
|
||||
)
|
||||
hour=4, minute=59, second=0
|
||||
) + timedelta(2)
|
||||
|
||||
with patch(
|
||||
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away
|
||||
@ -98,7 +174,7 @@ async def test_if_fires_using_multiple_at(hass, calls):
|
||||
now = dt_util.utcnow()
|
||||
|
||||
async_fire_time_changed(
|
||||
hass, now.replace(year=now.year + 1, hour=5, minute=0, second=0)
|
||||
hass, now.replace(hour=5, minute=0, second=0) + timedelta(2)
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
@ -106,7 +182,7 @@ async def test_if_fires_using_multiple_at(hass, calls):
|
||||
assert calls[0].data["some"] == "time - 5"
|
||||
|
||||
async_fire_time_changed(
|
||||
hass, now.replace(year=now.year + 1, hour=6, minute=0, second=0)
|
||||
hass, now.replace(hour=6, minute=0, second=0) + timedelta(2)
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
Loading…
x
Reference in New Issue
Block a user