core/homeassistant/components/automation/time.py

124 lines
4.0 KiB
Python

"""Offer time listening automation rules."""
from datetime import datetime
import logging
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_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, [_TIME_TRIGGER_SCHEMA]),
}
)
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
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}})
@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,
hour=at_time.hour,
minute=at_time.minute,
second=at_time.second,
)
)
# Track state changes of any entities.
removes.append(
async_track_state_change(hass, list(entities), update_entity_trigger)
)
@callback
def remove_track_time_changes():
"""Remove tracked time changes."""
for remove in removes:
remove()
return remove_track_time_changes