Beispiel #1
0
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
Beispiel #2
0
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)
Beispiel #3
0
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
Beispiel #4
0
    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)
Beispiel #5
0
        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)
Beispiel #7
0
 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)
Beispiel #8
0
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)
Beispiel #9
0
    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)
Beispiel #10
0
    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)
Beispiel #11
0
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)
Beispiel #12
0
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
Beispiel #13
0
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)
Beispiel #15
0
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