from datetime import datetime, timedelta from typing import Any, Optional # from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, HassJob from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.helpers.event import ( async_track_time_interval, async_track_point_in_utc_time, ) # For targeted patching in tests time_tracker_utcnow = dt_util.utcnow __ALL__ = ["async_track_time_change_in_tz"] @callback @bind_hass def async_track_utc_time_change( hass: HomeAssistant, action: None, hour: Optional[Any] = None, minute: Optional[Any] = None, second: Optional[Any] = None, tz: Optional[Any] = None, ) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" # This is function is modifies to support timezones. # We do not have to wrap the function with time pattern matching logic # if no pattern given if all(val is None for val in (hour, minute, second)): # Previously this relied on EVENT_TIME_FIRED # which meant it would not fire right away because # the caller would always be misaligned with the call # time vs the fire time by < 1s. To preserve this # misalignment we use async_track_time_interval here return async_track_time_interval(hass, action, timedelta(seconds=1)) job = HassJob(action) matching_seconds = dt_util.parse_time_expression(second, 0, 59) matching_minutes = dt_util.parse_time_expression(minute, 0, 59) matching_hours = dt_util.parse_time_expression(hour, 0, 23) def calculate_next(now: datetime) -> datetime: """Calculate and set the next time the trigger should fire.""" ts_now = now.astimezone(tz) if tz else now return dt_util.find_next_time_expression_time( ts_now, matching_seconds, matching_minutes, matching_hours ) time_listener: CALLBACK_TYPE | None = None @callback def pattern_time_change_listener(_: datetime) -> None: """Listen for matching time_changed events.""" nonlocal time_listener now = time_tracker_utcnow() hass.async_run_hass_job(job, now.astimezone(tz) if tz else now) time_listener = async_track_point_in_utc_time( hass, pattern_time_change_listener, calculate_next(now + timedelta(seconds=1)), ) time_listener = async_track_point_in_utc_time( hass, pattern_time_change_listener, calculate_next(dt_util.utcnow()) ) @callback def unsub_pattern_time_change_listener() -> None: """Cancel the time listener.""" assert time_listener is not None time_listener() return unsub_pattern_time_change_listener @callback @bind_hass def async_track_time_change_in_tz( hass: HomeAssistant, action: None, # action: Callable[[datetime], Awaitable[None] | None], hour: Optional[Any] = None, minute: Optional[Any] = None, second: Optional[Any] = None, tz: Optional[Any] = None, ) -> CALLBACK_TYPE: """Add a listener that will fire if UTC time matches a pattern.""" return async_track_utc_time_change(hass, action, hour, minute, second, tz)