diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index ecbf88d67a9..d418f9e62b1 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,4 +1,5 @@ """Helpers for listening to events.""" +import asyncio from datetime import datetime, timedelta import functools as ft import logging @@ -590,7 +591,8 @@ def async_track_utc_time_change( matching_minutes = dt_util.parse_time_expression(minute, 0, 59) matching_hours = dt_util.parse_time_expression(hour, 0, 23) - next_time = None + last_now: datetime = dt_util.utcnow() + next_time: datetime = last_now def calculate_next(now: datetime) -> None: """Calculate and set the next time the trigger should fire.""" @@ -603,29 +605,44 @@ def async_track_utc_time_change( # Make sure rolling back the clock doesn't prevent the timer from # triggering. - last_now: Optional[datetime] = None + cancel_callback: Optional[asyncio.TimerHandle] = None + calculate_next(last_now) @callback - def pattern_time_change_listener(event: Event) -> None: + def pattern_time_change_listener() -> None: """Listen for matching time_changed events.""" - nonlocal next_time, last_now + nonlocal next_time, last_now, cancel_callback - now = event.data[ATTR_NOW] + now = dt_util.utcnow() - if last_now is None or now < last_now: - # Time rolled back or next time not yet calculated + if now < last_now: + # Time rolled back 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) + last_now = now + + cancel_callback = hass.loop.call_at( + hass.loop.time() + next_time.timestamp() - time.time(), + pattern_time_change_listener, + ) + + cancel_callback = hass.loop.call_at( + hass.loop.time() + next_time.timestamp() - time.time(), + 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 track_utc_time_change = threaded_listener_factory(async_track_utc_time_change) diff --git a/tests/common.py b/tests/common.py index 5fa2ba59ed1..82d72c11a04 100644 --- a/tests/common.py +++ b/tests/common.py @@ -299,8 +299,11 @@ def async_fire_time_changed(hass, datetime_): mock_seconds_into_future = datetime_.timestamp() - time.time() if mock_seconds_into_future >= future_seconds: - task._run() - task.cancel() + with patch( + "homeassistant.util.dt.utcnow", return_value=date_util.as_utc(datetime_) + ): + task._run() + task.cancel() fire_time_changed = threadsafe_callback_factory(async_fire_time_changed) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 99b4cad6eca..9de9818eaf3 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -748,22 +748,24 @@ async def test_async_track_time_change(hass): wildcard_runs = [] specific_runs = [] + now = dt_util.utcnow() + unsub = async_track_time_change(hass, lambda x: wildcard_runs.append(1)) unsub_utc = async_track_utc_time_change( hass, lambda x: specific_runs.append(1), second=[0, 30] ) - async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 12, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 assert len(wildcard_runs) == 1 - async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 0, 15)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 12, 0, 15)) await hass.async_block_till_done() assert len(specific_runs) == 1 assert len(wildcard_runs) == 2 - async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 0, 30)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 12, 0, 30)) await hass.async_block_till_done() assert len(specific_runs) == 2 assert len(wildcard_runs) == 3 @@ -771,7 +773,7 @@ async def test_async_track_time_change(hass): unsub() unsub_utc() - async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 0, 30)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 12, 0, 30)) await hass.async_block_till_done() assert len(specific_runs) == 2 assert len(wildcard_runs) == 3 @@ -781,25 +783,27 @@ async def test_periodic_task_minute(hass): """Test periodic tasks per minute.""" specific_runs = [] + now = dt_util.utcnow() + unsub = async_track_utc_time_change( hass, lambda x: specific_runs.append(1), minute="/5", second=0 ) - async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 12, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 3, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 12, 3, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 5, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 12, 5, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 unsub() - async_fire_time_changed(hass, datetime(2014, 5, 24, 12, 5, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 12, 5, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 @@ -808,33 +812,35 @@ async def test_periodic_task_hour(hass): """Test periodic tasks per hour.""" specific_runs = [] + now = dt_util.utcnow() + unsub = async_track_utc_time_change( hass, lambda x: specific_runs.append(1), hour="/2", minute=0, second=0 ) - async_fire_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 22, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - async_fire_time_changed(hass, datetime(2014, 5, 24, 23, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 23, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - async_fire_time_changed(hass, datetime(2014, 5, 25, 0, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 25, 0, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 - async_fire_time_changed(hass, datetime(2014, 5, 25, 1, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 25, 1, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 - async_fire_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 25, 2, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 3 unsub() - async_fire_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 25, 2, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 3 @@ -843,12 +849,14 @@ async def test_periodic_task_wrong_input(hass): """Test periodic tasks with wrong input.""" specific_runs = [] + now = dt_util.utcnow() + with pytest.raises(ValueError): async_track_utc_time_change( hass, lambda x: specific_runs.append(1), hour="/two" ) - async_fire_time_changed(hass, datetime(2014, 5, 2, 0, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 2, 0, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 0 @@ -857,33 +865,35 @@ async def test_periodic_task_clock_rollback(hass): """Test periodic tasks with the time rolling backwards.""" specific_runs = [] + now = dt_util.utcnow() + unsub = async_track_utc_time_change( hass, lambda x: specific_runs.append(1), hour="/2", minute=0, second=0 ) - async_fire_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 22, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - async_fire_time_changed(hass, datetime(2014, 5, 24, 23, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 23, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - async_fire_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 22, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 - async_fire_time_changed(hass, datetime(2014, 5, 24, 0, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 0, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 3 - async_fire_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 25, 2, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 4 unsub() - async_fire_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 25, 2, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 4 @@ -892,19 +902,21 @@ async def test_periodic_task_duplicate_time(hass): """Test periodic tasks not triggering on duplicate time.""" specific_runs = [] + now = dt_util.utcnow() + unsub = async_track_utc_time_change( hass, lambda x: specific_runs.append(1), hour="/2", minute=0, second=0 ) - async_fire_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 22, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - async_fire_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 24, 22, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 1 - async_fire_time_changed(hass, datetime(2014, 5, 25, 0, 0, 0)) + async_fire_time_changed(hass, datetime(now.year + 1, 5, 25, 0, 0, 0)) await hass.async_block_till_done() assert len(specific_runs) == 2 @@ -917,23 +929,33 @@ async def test_periodic_task_entering_dst(hass): dt_util.set_default_time_zone(timezone) specific_runs = [] + now = dt_util.utcnow() + unsub = async_track_time_change( hass, lambda x: specific_runs.append(1), hour=2, minute=30, second=0 ) - async_fire_time_changed(hass, timezone.localize(datetime(2018, 3, 25, 1, 50, 0))) + async_fire_time_changed( + hass, timezone.localize(datetime(now.year + 1, 3, 25, 1, 50, 0)) + ) await hass.async_block_till_done() assert len(specific_runs) == 0 - async_fire_time_changed(hass, timezone.localize(datetime(2018, 3, 25, 3, 50, 0))) + async_fire_time_changed( + hass, timezone.localize(datetime(now.year + 1, 3, 25, 3, 50, 0)) + ) await hass.async_block_till_done() assert len(specific_runs) == 0 - async_fire_time_changed(hass, timezone.localize(datetime(2018, 3, 26, 1, 50, 0))) + async_fire_time_changed( + hass, timezone.localize(datetime(now.year + 1, 3, 26, 1, 50, 0)) + ) await hass.async_block_till_done() assert len(specific_runs) == 0 - async_fire_time_changed(hass, timezone.localize(datetime(2018, 3, 26, 2, 50, 0))) + async_fire_time_changed( + hass, timezone.localize(datetime(now.year + 1, 3, 26, 2, 50, 0)) + ) await hass.async_block_till_done() assert len(specific_runs) == 1 @@ -946,30 +968,32 @@ async def test_periodic_task_leaving_dst(hass): dt_util.set_default_time_zone(timezone) specific_runs = [] + now = dt_util.utcnow() + unsub = async_track_time_change( hass, lambda x: specific_runs.append(1), hour=2, minute=30, second=0 ) async_fire_time_changed( - hass, timezone.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=False) + hass, timezone.localize(datetime(now.year + 1, 10, 28, 2, 5, 0), is_dst=False) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, timezone.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=False) + hass, timezone.localize(datetime(now.year + 1, 10, 28, 2, 55, 0), is_dst=False) ) await hass.async_block_till_done() assert len(specific_runs) == 1 async_fire_time_changed( - hass, timezone.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=True) + hass, timezone.localize(datetime(now.year + 1, 10, 28, 2, 5, 0), is_dst=True) ) await hass.async_block_till_done() assert len(specific_runs) == 1 async_fire_time_changed( - hass, timezone.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True) + hass, timezone.localize(datetime(now.year + 1, 10, 28, 2, 55, 0), is_dst=True) ) await hass.async_block_till_done() assert len(specific_runs) == 2