From 60009ec2f96001fd95713f0a835feac77513d156 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Jul 2020 20:18:31 -1000 Subject: [PATCH] Use event loop scheduling for tracking time patterns (#38021) * Use event loop scheduling for tracking time patterns * make patching of time targetable * patch time tests since time can tick to match during the test * fix more tests * time can only move forward * time can only move forward * back to 100% coverage * simplify since the event loop time cannot move backwards * simplify some more * revert simplify * Revert "revert simplify" This reverts commit bd42f232f6f4e0876fbfe7ce5abf363779761acd. * Revert "simplify some more" This reverts commit 2a6c57d51487d4fc85140033a5cadbc800e61287. * Revert "simplify since the event loop time cannot move backwards" This reverts commit 3b13714ef489b5c5661ae0254b12f137d047fc26. * Attempt another simplify * time does not move backwards in the last two * remove next_time <= now check * fix previous merge error --- homeassistant/helpers/event.py | 44 ++- tests/common.py | 12 +- tests/components/automation/test_time.py | 6 +- .../automation/test_time_pattern.py | 322 +++++++++++------- tests/components/recorder/test_init.py | 16 +- tests/components/utility_meter/test_sensor.py | 18 +- tests/components/zwave/test_init.py | 2 +- tests/conftest.py | 64 ++++ tests/helpers/test_event.py | 119 +++++-- 9 files changed, 404 insertions(+), 199 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index ecbf88d67a9..3f0c2db3b2f 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 @@ -563,6 +564,9 @@ def async_track_sunset( track_sunset = threaded_listener_factory(async_track_sunset) +# For targeted patching in tests +pattern_utc_now = dt_util.utcnow + @callback @bind_hass @@ -590,7 +594,7 @@ 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 + next_time: datetime = dt_util.utcnow() def calculate_next(now: datetime) -> None: """Calculate and set the next time the trigger should fire.""" @@ -603,29 +607,37 @@ 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(next_time) @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, cancel_callback - now = event.data[ATTR_NOW] + now = pattern_utc_now() + hass.async_run_job(action, dt_util.as_local(now) if local else now) - if last_now is None or now < last_now: - # Time rolled back or next time not yet calculated - calculate_next(now) + calculate_next(now + timedelta(seconds=1)) - last_now = now + cancel_callback = hass.loop.call_at( + hass.loop.time() + next_time.timestamp() - time.time(), + pattern_time_change_listener, + ) - if next_time <= 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( + hass.loop.time() + next_time.timestamp() - time.time(), + pattern_time_change_listener, + ) - # 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) + @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..bcb66428f6b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -285,7 +285,7 @@ fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) @ha.callback -def async_fire_time_changed(hass, datetime_): +def async_fire_time_changed(hass, datetime_, fire_all=False): """Fire a time changes event.""" hass.bus.async_fire(EVENT_TIME_CHANGED, {"now": date_util.as_utc(datetime_)}) @@ -298,9 +298,13 @@ def async_fire_time_changed(hass, datetime_): future_seconds = task.when() - hass.loop.time() mock_seconds_into_future = datetime_.timestamp() - time.time() - if mock_seconds_into_future >= future_seconds: - task._run() - task.cancel() + if fire_all or mock_seconds_into_future >= future_seconds: + with patch( + "homeassistant.helpers.event.pattern_utc_now", + 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/components/automation/test_time.py b/tests/components/automation/test_time.py index 0ba85467fcd..81f1657e0a2 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -46,7 +46,11 @@ async def test_if_fires_using_at(hass, calls): }, ) - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=5, minute=0, second=0)) + now = dt_util.utcnow() + + async_fire_time_changed( + hass, now.replace(year=now.year + 1, hour=5, minute=0, second=0) + ) await hass.async_block_till_done() assert len(calls) == 1 diff --git a/tests/components/automation/test_time_pattern.py b/tests/components/automation/test_time_pattern.py index 01aa32f318f..b5141f088e4 100644 --- a/tests/components/automation/test_time_pattern.py +++ b/tests/components/automation/test_time_pattern.py @@ -1,4 +1,5 @@ """The tests for the time_pattern automation.""" +from asynctest.mock import patch import pytest import homeassistant.components.automation as automation @@ -23,53 +24,67 @@ def setup_comp(hass): async def test_if_fires_when_hour_matches(hass, calls): """Test for firing if hour is matching.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "time_pattern", - "hours": 0, - "minutes": "*", - "seconds": "*", - }, - "action": {"service": "test.automation"}, - } - }, + now = dt_util.utcnow() + time_that_will_not_match_right_away = dt_util.utcnow().replace( + year=now.year + 1, hour=3 ) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "time_pattern", + "hours": 0, + "minutes": "*", + "seconds": "*", + }, + "action": {"service": "test.automation"}, + } + }, + ) - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0)) + async_fire_time_changed(hass, now.replace(year=now.year + 2, hour=0)) await hass.async_block_till_done() assert len(calls) == 1 await common.async_turn_off(hass) await hass.async_block_till_done() - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0)) + async_fire_time_changed(hass, now.replace(year=now.year + 1, hour=0)) await hass.async_block_till_done() assert len(calls) == 1 async def test_if_fires_when_minute_matches(hass, calls): """Test for firing if minutes are matching.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "time_pattern", - "hours": "*", - "minutes": 0, - "seconds": "*", - }, - "action": {"service": "test.automation"}, - } - }, + now = dt_util.utcnow() + time_that_will_not_match_right_away = dt_util.utcnow().replace( + year=now.year + 1, minute=30 ) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "time_pattern", + "hours": "*", + "minutes": 0, + "seconds": "*", + }, + "action": {"service": "test.automation"}, + } + }, + ) - async_fire_time_changed(hass, dt_util.utcnow().replace(minute=0)) + async_fire_time_changed(hass, now.replace(year=now.year + 2, minute=0)) await hass.async_block_till_done() assert len(calls) == 1 @@ -77,23 +92,30 @@ async def test_if_fires_when_minute_matches(hass, calls): async def test_if_fires_when_second_matches(hass, calls): """Test for firing if seconds are matching.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "time_pattern", - "hours": "*", - "minutes": "*", - "seconds": 0, - }, - "action": {"service": "test.automation"}, - } - }, + now = dt_util.utcnow() + time_that_will_not_match_right_away = dt_util.utcnow().replace( + year=now.year + 1, second=30 ) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "time_pattern", + "hours": "*", + "minutes": "*", + "seconds": 0, + }, + "action": {"service": "test.automation"}, + } + }, + ) - async_fire_time_changed(hass, dt_util.utcnow().replace(second=0)) + async_fire_time_changed(hass, now.replace(year=now.year + 2, second=0)) await hass.async_block_till_done() assert len(calls) == 1 @@ -101,23 +123,32 @@ async def test_if_fires_when_second_matches(hass, calls): async def test_if_fires_when_all_matches(hass, calls): """Test for firing if everything matches.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "time_pattern", - "hours": 1, - "minutes": 2, - "seconds": 3, - }, - "action": {"service": "test.automation"}, - } - }, + now = dt_util.utcnow() + time_that_will_not_match_right_away = dt_util.utcnow().replace( + year=now.year + 1, hour=4 ) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "time_pattern", + "hours": 1, + "minutes": 2, + "seconds": 3, + }, + "action": {"service": "test.automation"}, + } + }, + ) - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=1, minute=2, second=3)) + async_fire_time_changed( + hass, now.replace(year=now.year + 2, hour=1, minute=2, second=3) + ) await hass.async_block_till_done() assert len(calls) == 1 @@ -125,47 +156,66 @@ async def test_if_fires_when_all_matches(hass, calls): async def test_if_fires_periodic_seconds(hass, calls): """Test for firing periodically every second.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "time_pattern", - "hours": "*", - "minutes": "*", - "seconds": "/2", - }, - "action": {"service": "test.automation"}, - } - }, + now = dt_util.utcnow() + time_that_will_not_match_right_away = dt_util.utcnow().replace( + year=now.year + 1, second=1 + ) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "time_pattern", + "hours": "*", + "minutes": "*", + "seconds": "/10", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + async_fire_time_changed( + hass, now.replace(year=now.year + 2, hour=0, minute=0, second=10) ) - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0, minute=0, second=2)) - await hass.async_block_till_done() - assert len(calls) == 1 + assert len(calls) >= 1 async def test_if_fires_periodic_minutes(hass, calls): """Test for firing periodically every minute.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "time_pattern", - "hours": "*", - "minutes": "/2", - "seconds": "*", - }, - "action": {"service": "test.automation"}, - } - }, - ) - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0, minute=2, second=0)) + now = dt_util.utcnow() + time_that_will_not_match_right_away = dt_util.utcnow().replace( + year=now.year + 1, minute=1 + ) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "time_pattern", + "hours": "*", + "minutes": "/2", + "seconds": "*", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + async_fire_time_changed( + hass, now.replace(year=now.year + 2, hour=0, minute=2, second=0) + ) await hass.async_block_till_done() assert len(calls) == 1 @@ -173,23 +223,32 @@ async def test_if_fires_periodic_minutes(hass, calls): async def test_if_fires_periodic_hours(hass, calls): """Test for firing periodically every hour.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "time_pattern", - "hours": "/2", - "minutes": "*", - "seconds": "*", - }, - "action": {"service": "test.automation"}, - } - }, + now = dt_util.utcnow() + time_that_will_not_match_right_away = dt_util.utcnow().replace( + year=now.year + 1, hour=1 ) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "time_pattern", + "hours": "/2", + "minutes": "*", + "seconds": "*", + }, + "action": {"service": "test.automation"}, + } + }, + ) - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=2, minute=0, second=0)) + async_fire_time_changed( + hass, now.replace(year=now.year + 2, hour=2, minute=0, second=0) + ) await hass.async_block_till_done() assert len(calls) == 1 @@ -197,28 +256,41 @@ async def test_if_fires_periodic_hours(hass, calls): async def test_default_values(hass, calls): """Test for firing at 2 minutes every hour.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "time_pattern", "minutes": "2"}, - "action": {"service": "test.automation"}, - } - }, + now = dt_util.utcnow() + time_that_will_not_match_right_away = dt_util.utcnow().replace( + year=now.year + 1, minute=1 + ) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "time_pattern", "minutes": "2"}, + "action": {"service": "test.automation"}, + } + }, + ) + + async_fire_time_changed( + hass, now.replace(year=now.year + 2, hour=1, minute=2, second=0) ) - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=1, minute=2, second=0)) + await hass.async_block_till_done() + assert len(calls) == 1 + + async_fire_time_changed( + hass, now.replace(year=now.year + 2, hour=1, minute=2, second=1) + ) await hass.async_block_till_done() assert len(calls) == 1 - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=1, minute=2, second=1)) - - await hass.async_block_till_done() - assert len(calls) == 1 - - async_fire_time_changed(hass, dt_util.utcnow().replace(hour=2, minute=2, second=0)) + async_fire_time_changed( + hass, now.replace(year=now.year + 2, hour=2, minute=2, second=0) + ) await hass.async_block_till_done() assert len(calls) == 2 diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 46db4782628..a4f70fc09a6 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -17,14 +17,18 @@ from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import Events, RecorderRuns, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import MATCH_ALL, STATE_LOCKED, STATE_UNLOCKED -from homeassistant.core import ATTR_NOW, EVENT_TIME_CHANGED, Context, callback +from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from .common import wait_recording_done from tests.async_mock import patch -from tests.common import get_test_home_assistant, init_recorder_component +from tests.common import ( + async_fire_time_changed, + get_test_home_assistant, + init_recorder_component, +) class TestRecorder(unittest.TestCase): @@ -335,15 +339,15 @@ def test_auto_purge(hass_recorder): tz = dt_util.get_time_zone("Europe/Copenhagen") dt_util.set_default_time_zone(tz) - test_time = tz.localize(datetime(2020, 1, 1, 4, 12, 0)) + now = dt_util.utcnow() + test_time = tz.localize(datetime(now.year + 1, 1, 1, 4, 12, 0)) + async_fire_time_changed(hass, test_time) with patch( "homeassistant.components.recorder.purge.purge_old_data", return_value=True ) as purge_old_data: for delta in (-1, 0, 1): - hass.bus.fire( - EVENT_TIME_CHANGED, {ATTR_NOW: test_time + timedelta(seconds=delta)} - ) + async_fire_time_changed(hass, test_time + timedelta(seconds=delta)) hass.block_till_done() hass.data[DATA_INSTANCE].block_till_done() diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 09145fc4e4e..c1613c53a20 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -260,49 +260,49 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): assert state.state == "5" -async def test_self_reset_hourly(hass): +async def test_self_reset_hourly(hass, legacy_patchable_time): """Test hourly reset of meter.""" await _test_self_reset( hass, gen_config("hourly"), "2017-12-31T23:59:00.000000+00:00" ) -async def test_self_reset_daily(hass): +async def test_self_reset_daily(hass, legacy_patchable_time): """Test daily reset of meter.""" await _test_self_reset( hass, gen_config("daily"), "2017-12-31T23:59:00.000000+00:00" ) -async def test_self_reset_weekly(hass): +async def test_self_reset_weekly(hass, legacy_patchable_time): """Test weekly reset of meter.""" await _test_self_reset( hass, gen_config("weekly"), "2017-12-31T23:59:00.000000+00:00" ) -async def test_self_reset_monthly(hass): +async def test_self_reset_monthly(hass, legacy_patchable_time): """Test monthly reset of meter.""" await _test_self_reset( hass, gen_config("monthly"), "2017-12-31T23:59:00.000000+00:00" ) -async def test_self_reset_quarterly(hass): +async def test_self_reset_quarterly(hass, legacy_patchable_time): """Test quarterly reset of meter.""" await _test_self_reset( hass, gen_config("quarterly"), "2017-03-31T23:59:00.000000+00:00" ) -async def test_self_reset_yearly(hass): +async def test_self_reset_yearly(hass, legacy_patchable_time): """Test yearly reset of meter.""" await _test_self_reset( hass, gen_config("yearly"), "2017-12-31T23:59:00.000000+00:00" ) -async def test_self_no_reset_yearly(hass): +async def test_self_no_reset_yearly(hass, legacy_patchable_time): """Test yearly reset of meter does not occur after 1st January.""" await _test_self_reset( hass, @@ -312,7 +312,7 @@ async def test_self_no_reset_yearly(hass): ) -async def test_reset_yearly_offset(hass): +async def test_reset_yearly_offset(hass, legacy_patchable_time): """Test yearly reset of meter.""" await _test_self_reset( hass, @@ -321,7 +321,7 @@ async def test_reset_yearly_offset(hass): ) -async def test_no_reset_yearly_offset(hass): +async def test_no_reset_yearly_offset(hass, legacy_patchable_time): """Test yearly reset of meter.""" await _test_self_reset( hass, diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index d1f141582ca..12b2c59ca81 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -119,7 +119,7 @@ async def test_erronous_network_key_fails_validation(hass, mock_openzwave): zwave.CONFIG_SCHEMA({"zwave": {"network_key": value}}) -async def test_auto_heal_midnight(hass, mock_openzwave): +async def test_auto_heal_midnight(hass, mock_openzwave, legacy_patchable_time): """Test network auto-heal at midnight.""" await async_setup_component(hass, "zwave", {"zwave": {"autoheal": True}}) await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index c3f600f9693..5c90dcb063e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ """Set up some common test helper things.""" import asyncio +import datetime import functools import logging import threading @@ -387,8 +388,71 @@ def legacy_patchable_time(): return async_unsub + @ha.callback + @loader.bind_hass + def async_track_utc_time_change( + hass, action, hour=None, minute=None, second=None, local=False + ): + """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)): + + @ha.callback + def time_change_listener(ev) -> None: + """Fire every time event that comes in.""" + hass.async_run_job(action, ev.data[ATTR_NOW]) + + return hass.bus.async_listen(EVENT_TIME_CHANGED, time_change_listener) + + matching_seconds = event.dt_util.parse_time_expression(second, 0, 59) + matching_minutes = event.dt_util.parse_time_expression(minute, 0, 59) + matching_hours = event.dt_util.parse_time_expression(hour, 0, 23) + + next_time = None + + def calculate_next(now) -> None: + """Calculate and set the next time the trigger should fire.""" + nonlocal next_time + + localized_now = event.dt_util.as_local(now) if local else now + next_time = event.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 = None + + @ha.callback + def pattern_time_change_listener(ev) -> None: + """Listen for matching time_changed events.""" + nonlocal next_time, last_now + + now = ev.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, event.dt_util.as_local(now) if local else now + ) + calculate_next(now + datetime.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) + with patch( "homeassistant.helpers.event.async_track_point_in_utc_time", async_track_point_in_utc_time, + ), patch( + "homeassistant.helpers.event.async_track_utc_time_change", + async_track_utc_time_change, ): yield diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 99b4cad6eca..784bb673f77 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,37 @@ 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), fire_all=True + ) 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), fire_all=True) 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 +904,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 +931,39 @@ async def test_periodic_task_entering_dst(hass): dt_util.set_default_time_zone(timezone) specific_runs = [] - unsub = async_track_time_change( - hass, lambda x: specific_runs.append(1), hour=2, minute=30, second=0 + now = dt_util.utcnow() + time_that_will_not_match_right_away = timezone.localize( + datetime(now.year + 1, 3, 25, 2, 31, 0) ) - async_fire_time_changed(hass, timezone.localize(datetime(2018, 3, 25, 1, 50, 0))) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + 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(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 +976,45 @@ async def test_periodic_task_leaving_dst(hass): dt_util.set_default_time_zone(timezone) specific_runs = [] - unsub = async_track_time_change( - hass, lambda x: specific_runs.append(1), hour=2, minute=30, second=0 + now = dt_util.utcnow() + + time_that_will_not_match_right_away = timezone.localize( + datetime(now.year + 1, 10, 28, 2, 28, 0), is_dst=True ) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + 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 + 2, 10, 28, 2, 45, 0), is_dst=True) ) await hass.async_block_till_done() - assert len(specific_runs) == 1 + assert len(specific_runs) == 2 async_fire_time_changed( - hass, timezone.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True) + hass, timezone.localize(datetime(now.year + 2, 10, 28, 2, 55, 0), is_dst=True) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + async_fire_time_changed( + hass, timezone.localize(datetime(now.year + 2, 10, 28, 2, 55, 0), is_dst=True) ) await hass.async_block_till_done() assert len(specific_runs) == 2