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 bd42f232f6.

* Revert "simplify some more"

This reverts commit 2a6c57d514.

* Revert "simplify since the event loop time cannot move backwards"

This reverts commit 3b13714ef4.

* Attempt another simplify

* time does not move backwards in the last two

* remove next_time <= now check

* fix previous merge error
This commit is contained in:
J. Nick Koston 2020-07-20 20:18:31 -10:00 committed by GitHub
parent 7bc8caca96
commit 60009ec2f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 404 additions and 199 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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,

View file

@ -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()

View file

@ -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

View file

@ -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