mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +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."""
|
"""Offer time listening automation rules."""
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -6,30 +7,99 @@ import voluptuous as vol
|
|||||||
from homeassistant.const import CONF_AT, CONF_PLATFORM
|
from homeassistant.const import CONF_AT, CONF_PLATFORM
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
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
|
# mypy: allow-untyped-defs, no-check-untyped-defs
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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(
|
TRIGGER_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_PLATFORM): "time",
|
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):
|
async def async_attach_trigger(hass, config, action, automation_info):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
at_times = config[CONF_AT]
|
entities = {}
|
||||||
|
removes = []
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def time_automation_listener(now):
|
def time_automation_listener(now):
|
||||||
"""Listen for time changes and calls action."""
|
"""Listen for time changes and calls action."""
|
||||||
hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}})
|
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(
|
async_track_time_change(
|
||||||
hass,
|
hass,
|
||||||
time_automation_listener,
|
time_automation_listener,
|
||||||
@ -37,8 +107,12 @@ async def async_attach_trigger(hass, config, action, automation_info):
|
|||||||
minute=at_time.minute,
|
minute=at_time.minute,
|
||||||
second=at_time.second,
|
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
|
@callback
|
||||||
def remove_track_time_changes():
|
def remove_track_time_changes():
|
||||||
|
@ -34,8 +34,8 @@ async def test_if_fires_using_at(hass, calls):
|
|||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
|
|
||||||
time_that_will_not_match_right_away = now.replace(
|
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(
|
with patch(
|
||||||
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away
|
"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()
|
now = dt_util.utcnow()
|
||||||
|
|
||||||
async_fire_time_changed(
|
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()
|
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"
|
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):
|
async def test_if_fires_using_multiple_at(hass, calls):
|
||||||
"""Test for firing at."""
|
"""Test for firing at."""
|
||||||
|
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
|
|
||||||
time_that_will_not_match_right_away = now.replace(
|
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(
|
with patch(
|
||||||
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away
|
"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()
|
now = dt_util.utcnow()
|
||||||
|
|
||||||
async_fire_time_changed(
|
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()
|
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"
|
assert calls[0].data["some"] == "time - 5"
|
||||||
|
|
||||||
async_fire_time_changed(
|
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()
|
await hass.async_block_till_done()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user