1 """Offer time listening automation rules."""
3 from collections.abc
import Callable
4 from datetime
import datetime, timedelta
5 from functools
import partial
6 from typing
import Any, NamedTuple
8 import voluptuous
as vol
23 EventStateChangedData,
32 async_track_point_in_time,
33 async_track_state_change_event,
34 async_track_time_change,
40 _TIME_TRIGGER_ENTITY = vol.All(str, cv.entity_domain([
"input_datetime",
"sensor"]))
41 _TIME_AT_SCHEMA = vol.Any(cv.time, _TIME_TRIGGER_ENTITY)
43 _TIME_TRIGGER_ENTITY_WITH_OFFSET = vol.Schema(
45 vol.Required(CONF_ENTITY_ID): cv.entity_domain([
"sensor"]),
46 vol.Optional(CONF_OFFSET): cv.time_period,
52 """Validate either a jinja2 template, valid time, or valid trigger entity."""
53 tpl = cv.template(value)
61 _TIME_TRIGGER_SCHEMA = vol.Any(
64 _TIME_TRIGGER_ENTITY_WITH_OFFSET,
67 "Expected HH:MM, HH:MM:SS, an Entity ID with domain 'input_datetime' or "
68 "'sensor', a combination of a timestamp sensor entity and an offset, or Limited Template"
73 TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
75 vol.Required(CONF_PLATFORM):
"time",
76 vol.Required(CONF_AT): vol.All(cv.ensure_list, [_TIME_TRIGGER_SCHEMA]),
82 """Represents a tracking entity for a time trigger."""
91 action: TriggerActionType,
92 trigger_info: TriggerInfo,
94 """Listen for state changes based on configuration."""
95 trigger_data = trigger_info[
"trigger_data"]
96 variables = trigger_info[
"variables"]
or {}
97 entities: dict[tuple[str, timedelta], CALLBACK_TYPE] = {}
98 removes: list[CALLBACK_TYPE] = []
99 job =
HassJob(action, f
"time trigger {trigger_info}")
102 def time_automation_listener(
103 description: str, now: datetime, *, entity_id: str |
None =
None
105 """Listen for time changes and calls action."""
106 hass.async_run_hass_job(
113 "description": description,
114 "entity_id": entity_id,
120 def update_entity_trigger_event(
121 event: Event[EventStateChangedData], offset: timedelta =
timedelta(0)
123 """update_entity_trigger from the event."""
124 return update_entity_trigger(
125 event.data[
"entity_id"], event.data[
"new_state"], offset
129 def update_entity_trigger(
130 entity_id: str, new_state: State |
None =
None, offset: timedelta =
timedelta(0)
132 """Update the entity trigger for the entity_id."""
134 if remove := entities.pop((entity_id, offset),
None):
141 trigger_dt: datetime |
None
144 if new_state.domain ==
"input_datetime":
145 if has_date := new_state.attributes[
"has_date"]:
146 year = new_state.attributes[
"year"]
147 month = new_state.attributes[
"month"]
148 day = new_state.attributes[
"day"]
149 if has_time := new_state.attributes[
"has_time"]:
150 hour = new_state.attributes[
"hour"]
151 minute = new_state.attributes[
"minute"]
152 second = new_state.attributes[
"second"]
155 hour = minute = second = 0
166 tzinfo=dt_util.get_default_time_zone(),
169 if trigger_dt >= dt_util.now():
173 time_automation_listener,
174 f
"time set in {entity_id}",
184 time_automation_listener,
185 f
"time set in {entity_id}",
193 new_state.domain ==
"sensor"
194 and new_state.attributes.get(ATTR_DEVICE_CLASS)
195 == sensor.SensorDeviceClass.TIMESTAMP
196 and new_state.state
not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
198 trigger_dt = dt_util.parse_datetime(new_state.state)
200 if trigger_dt
is not None:
203 if trigger_dt
is not None and trigger_dt > dt_util.utcnow():
207 time_automation_listener,
208 f
"time set in {entity_id}",
216 entities[(entity_id, offset)] = remove
218 to_track: list[TrackEntity] = []
220 for at_time
in config[CONF_AT]:
221 if isinstance(at_time, template.Template):
222 render = template.render_complex(at_time, variables, limited=
True)
225 except vol.Invalid
as exc:
227 f
"Limited Template for 'at' rendered a unexpected value '{render}', expected HH:MM, "
228 f
"HH:MM:SS or Entity ID with domain 'input_datetime' or 'sensor'"
231 if isinstance(at_time, str):
233 update_entity_trigger(at_time, new_state=hass.states.get(at_time))
234 to_track.append(
TrackEntity(at_time, update_entity_trigger_event))
235 elif isinstance(at_time, dict)
and CONF_OFFSET
in at_time:
237 entity_id: str = at_time.get(CONF_ENTITY_ID,
"")
238 offset: timedelta = at_time.get(CONF_OFFSET,
timedelta(0))
239 update_entity_trigger(
240 entity_id, new_state=hass.states.get(entity_id), offset=offset
244 entity_id, partial(update_entity_trigger_event, offset=offset)
252 partial(time_automation_listener,
"time"),
254 minute=at_time.minute,
255 second=at_time.second,
262 for entry
in to_track
266 def remove_track_time_changes() -> None:
267 """Remove tracked time changes."""
268 for remove
in entities.values():
270 for remove
in removes:
273 return remove_track_time_changes
bool remove(self, _T matcher)
CALLBACK_TYPE async_attach_trigger(HomeAssistant hass, ConfigType config, TriggerActionType action, TriggerInfo trigger_info)
template.Template valid_at_template(Any value)
datetime_sys datetime(Any value)
CALLBACK_TYPE async_track_time_change(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, Any|None hour=None, Any|None minute=None, Any|None second=None)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
CALLBACK_TYPE async_track_point_in_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)