From f8704a2dfc25105c841c340570e36dbafe1a6878 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 09:27:25 -0500 Subject: [PATCH] Ensure we always fire time pattern changes after microsecond 0 (#39302) --- homeassistant/helpers/event.py | 35 ++++++++++++++++++++++++++++------ tests/util/test_dt.py | 4 ++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index cd436919997..21f49c008c8 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -34,6 +34,8 @@ from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe +MAX_TIME_TRACKING_ERROR = 0.001 + TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks" TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener" @@ -996,19 +998,40 @@ def async_track_utc_time_change( 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(), + -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 + # 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(), + -time.time() + + hass.loop.time() + + next_time.timestamp() + + MAX_TIME_TRACKING_ERROR, pattern_time_change_listener, ) diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index f693f3026c5..03d4ee53cbe 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -219,6 +219,10 @@ def test_find_next_time_expression_time_basic(): datetime(2018, 10, 7, 10, 30, 0), 5, 0, 0 ) + assert find(datetime(2018, 10, 7, 10, 30, 0, 999999), "*", "/30", 0) == datetime( + 2018, 10, 7, 10, 30, 0 + ) + def test_find_next_time_expression_time_dst(): """Test daylight saving time for find_next_time_expression_time."""