def async_track_utc_time_change( hass: HomeAssistant, action: Callable[..., None], hour: Optional[Any] = None, minute: Optional[Any] = None, second: Optional[Any] = None, local: bool = False, ) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" job = HassJob(action) # 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)): @callback def time_change_listener(event: Event) -> None: """Fire every time event that comes in.""" hass.async_run_hass_job(job, event.data[ATTR_NOW]) return hass.bus.async_listen(EVENT_TIME_CHANGED, time_change_listener) 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.""" localized_now = dt_util.as_local(now) if local else now return dt_util.find_next_time_expression_time(localized_now, matching_seconds, matching_minutes, matching_hours) time_listener: Optional[CALLBACK_TYPE] = 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, dt_util.as_local(now) if local 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
def async_track_utc_time_change( hass: HomeAssistant, action: Callable[..., None], hour: Optional[Any] = None, minute: Optional[Any] = None, second: Optional[Any] = None, local: bool = False, ) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" # 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)): @callback def time_change_listener(event: Event) -> None: """Fire every time event that comes in.""" hass.async_run_job(action, event.data[ATTR_NOW]) return hass.bus.async_listen(EVENT_TIME_CHANGED, time_change_listener) 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) next_time = None def calculate_next(now: datetime) -> None: """Calculate and set the next time the trigger should fire.""" nonlocal next_time localized_now = dt_util.as_local(now) if local else now next_time = dt_util.find_next_time_expression_time( localized_now, matching_seconds, matching_minutes, matching_hours) # Make sure rolling back the clock doesn't prevent the timer from # triggering. last_now: Optional[datetime] = None @callback def pattern_time_change_listener(event: Event) -> None: """Listen for matching time_changed events.""" nonlocal next_time, last_now now = event.data[ATTR_NOW] if last_now is None or now < last_now: # Time rolled back or next time not yet calculated calculate_next(now) last_now = now if next_time <= now: hass.async_run_job(action, dt_util.as_local(now) if local else now) calculate_next(now + timedelta(seconds=1)) # We can't use async_track_point_in_utc_time here because it would # break in the case that the system time abruptly jumps backwards. # Our custom last_now logic takes care of resolving that scenario. return hass.bus.async_listen(EVENT_TIME_CHANGED, pattern_time_change_listener)
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
def find(dt, hour, minute, second): """Call test_find_next_time_expression_time.""" seconds = dt_util.parse_time_expression(second, 0, 59) minutes = dt_util.parse_time_expression(minute, 0, 59) hours = dt_util.parse_time_expression(hour, 0, 23) return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours)
def find(dt, hour, minute, second): """Call test_find_next_time_expression_time.""" seconds = dt_util.parse_time_expression(second, 0, 59) minutes = dt_util.parse_time_expression(minute, 0, 59) hours = dt_util.parse_time_expression(hour, 0, 23) return dt_util.find_next_time_expression_time( dt, seconds, minutes, hours)
def test_find_next_time_expression_exiting_dst(now_dt, expected_dt): """Test exiting daylight saving time for find_next_time_expression_time.""" tz = dt_util.get_time_zone("Europe/Vienna") dt_util.set_default_time_zone(tz) # match on 02:30:00 every day pattern_seconds = dt_util.parse_time_expression(0, 0, 59) pattern_minutes = dt_util.parse_time_expression(30, 0, 59) pattern_hours = dt_util.parse_time_expression(2, 0, 59) now_dt = now_dt.replace(tzinfo=tz) expected_dt = expected_dt.replace(tzinfo=tz) res_dt = dt_util.find_next_time_expression_time(now_dt, pattern_seconds, pattern_minutes, pattern_hours) assert dt_util.as_utc(res_dt) == dt_util.as_utc(expected_dt)
def _round_to_next_five_minutes(now): """Rounds the provided time to the next 5 minutes.""" matching_seconds = [0] matching_minutes = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55] matching_hours = dt_util.parse_time_expression('*', 0, 23) return dt_util.find_next_time_expression_time(now, matching_seconds, matching_minutes, matching_hours)
def test_parse_time_expression(): """Test parse_time_expression.""" assert list(range(60)) == dt_util.parse_time_expression("*", 0, 59) assert list(range(60)) == dt_util.parse_time_expression(None, 0, 59) assert list(range(0, 60, 5)) == dt_util.parse_time_expression("/5", 0, 59) assert [1, 2, 3] == dt_util.parse_time_expression([2, 1, 3], 0, 59) assert list(range(24)) == dt_util.parse_time_expression("*", 0, 23) assert [42] == dt_util.parse_time_expression(42, 0, 59) assert [42] == dt_util.parse_time_expression("42", 0, 59) with pytest.raises(ValueError): dt_util.parse_time_expression(61, 0, 60)
def test_parse_time_expression(self): """Test parse_time_expression.""" self.assertEqual( [x for x in range(60)], dt_util.parse_time_expression('*', 0, 59) ) self.assertEqual( [x for x in range(60)], dt_util.parse_time_expression(None, 0, 59) ) self.assertEqual( [x for x in range(0, 60, 5)], dt_util.parse_time_expression('/5', 0, 59) ) self.assertEqual( [1, 2, 3], dt_util.parse_time_expression([2, 1, 3], 0, 59) ) self.assertEqual( [x for x in range(24)], dt_util.parse_time_expression('*', 0, 23) ) self.assertEqual( [42], dt_util.parse_time_expression(42, 0, 59) ) self.assertRaises(ValueError, dt_util.parse_time_expression, 61, 0, 60)
def test_parse_time_expression(self): """Test parse_time_expression.""" assert [x for x in range(60)] == \ dt_util.parse_time_expression('*', 0, 59) assert [x for x in range(60)] == \ dt_util.parse_time_expression(None, 0, 59) assert [x for x in range(0, 60, 5)] == \ dt_util.parse_time_expression('/5', 0, 59) assert [1, 2, 3] == \ dt_util.parse_time_expression([2, 1, 3], 0, 59) assert [x for x in range(24)] == \ dt_util.parse_time_expression('*', 0, 23) assert [42] == \ dt_util.parse_time_expression(42, 0, 59) with pytest.raises(ValueError): dt_util.parse_time_expression(61, 0, 60)
def test_parse_time_expression(): """Test parse_time_expression.""" assert [x for x in range(60)] == \ dt_util.parse_time_expression('*', 0, 59) assert [x for x in range(60)] == \ dt_util.parse_time_expression(None, 0, 59) assert [x for x in range(0, 60, 5)] == \ dt_util.parse_time_expression('/5', 0, 59) assert [1, 2, 3] == \ dt_util.parse_time_expression([2, 1, 3], 0, 59) assert [x for x in range(24)] == \ dt_util.parse_time_expression('*', 0, 23) assert [42] == \ dt_util.parse_time_expression(42, 0, 59) with pytest.raises(ValueError): dt_util.parse_time_expression(61, 0, 60)
def async_track_utc_time_change( hass: HomeAssistant, action: Callable[..., None], hour: Optional[Any] = None, minute: Optional[Any] = None, second: Optional[Any] = None, local: bool = False, ) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" # 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)): @callback def time_change_listener(event: Event) -> None: """Fire every time event that comes in.""" hass.async_run_job(action, event.data[ATTR_NOW]) return hass.bus.async_listen(EVENT_TIME_CHANGED, time_change_listener) 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) next_time: datetime = dt_util.utcnow() def calculate_next(now: datetime) -> None: """Calculate and set the next time the trigger should fire.""" nonlocal next_time localized_now = dt_util.as_local(now) if local else now next_time = dt_util.find_next_time_expression_time( localized_now, matching_seconds, matching_minutes, matching_hours) # Make sure rolling back the clock doesn't prevent the timer from # triggering. cancel_callback: Optional[asyncio.TimerHandle] = None calculate_next(next_time) @callback def pattern_time_change_listener() -> None: """Listen for matching time_changed events.""" nonlocal next_time, cancel_callback now = pattern_utc_now() hass.async_run_job(action, dt_util.as_local(now) if local else now) calculate_next(now + timedelta(seconds=1)) cancel_callback = hass.loop.call_at( -time.time() + hass.loop.time() + next_time.timestamp() + MAX_TIME_TRACKING_ERROR, pattern_time_change_listener, ) # We always get time.time() first to avoid time.time() # ticking forward after fetching hass.loop.time() # and callback being scheduled a few microseconds early. # # Since we loose additional time calling `hass.loop.time()` # we add MAX_TIME_TRACKING_ERROR to ensure # we always schedule the call within the time window between # second and the next second. # # For example: # If the clock ticks forward 30 microseconds when fectching # `hass.loop.time()` and we want the event to fire at exactly # 03:00:00.000000, the event would actually fire around # 02:59:59.999970. To ensure we always fire sometime between # 03:00:00.000000 and 03:00:00.999999 we add # MAX_TIME_TRACKING_ERROR to make up for the time # lost fetching the time. This ensures we do not fire the # event before the next time pattern match which would result # in the event being fired again since we would otherwise # potentially fire early. # cancel_callback = hass.loop.call_at( -time.time() + hass.loop.time() + next_time.timestamp() + MAX_TIME_TRACKING_ERROR, pattern_time_change_listener, ) @callback def unsub_pattern_time_change_listener() -> None: """Cancel the call_later.""" nonlocal cancel_callback assert cancel_callback is not None cancel_callback.cancel() return unsub_pattern_time_change_listener
def _get_matches(hours, minutes, seconds): matching_hours = dt_util.parse_time_expression(hours, 0, 23) matching_minutes = dt_util.parse_time_expression(minutes, 0, 59) matching_seconds = dt_util.parse_time_expression(seconds, 0, 59) return matching_hours, matching_minutes, matching_seconds
def rounder(t): matching_seconds = [0] matching_minutes = [2] matching_hours = dt_util.parse_time_expression("*", 0, 23) return dt_util.find_next_time_expression_time( t, matching_seconds, matching_minutes, matching_hours)
def async_track_utc_time_change( hass: HomeAssistant, action: Callable[..., None], hour: Optional[Any] = None, minute: Optional[Any] = None, second: Optional[Any] = None, local: bool = False, ) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" # 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)): @callback def time_change_listener(event: Event) -> None: """Fire every time event that comes in.""" hass.async_run_job(action, event.data[ATTR_NOW]) return hass.bus.async_listen(EVENT_TIME_CHANGED, time_change_listener) 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) next_time: datetime = dt_util.utcnow() def calculate_next(now: datetime) -> None: """Calculate and set the next time the trigger should fire.""" nonlocal next_time localized_now = dt_util.as_local(now) if local else now next_time = dt_util.find_next_time_expression_time( localized_now, matching_seconds, matching_minutes, matching_hours) # Make sure rolling back the clock doesn't prevent the timer from # triggering. cancel_callback: Optional[asyncio.TimerHandle] = None calculate_next(next_time) @callback def pattern_time_change_listener() -> None: """Listen for matching time_changed events.""" nonlocal next_time, cancel_callback now = pattern_utc_now() hass.async_run_job(action, dt_util.as_local(now) if local else now) calculate_next(now + timedelta(seconds=1)) # We always get time.time() first to avoid time.time() # ticking forward after fetching hass.loop.time() # and callback being scheduled a few microseconds early cancel_callback = hass.loop.call_at( -time.time() + hass.loop.time() + next_time.timestamp(), pattern_time_change_listener, ) # We always get time.time() first to avoid time.time() # ticking forward after fetching hass.loop.time() # and callback being scheduled a few microseconds early cancel_callback = hass.loop.call_at( -time.time() + hass.loop.time() + next_time.timestamp(), pattern_time_change_listener, ) @callback def unsub_pattern_time_change_listener() -> None: """Cancel the call_later.""" nonlocal cancel_callback assert cancel_callback is not None cancel_callback.cancel() return unsub_pattern_time_change_listener