Time trigger can also accept an input_datetime Entity ID (#38698)

This commit is contained in:
Phil Bruckner 2020-08-11 15:16:28 -05:00 committed by GitHub
parent dd86de3255
commit 192fe58fc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 170 additions and 20 deletions

View File

@ -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():

View File

@ -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()