From dc7e3a6df6c560ede9d03bf6711a63458e6531cf Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 24 Apr 2022 12:52:17 -0700 Subject: [PATCH] Fix boundary case in calednar trigger (#70467) Update calendar trigger scan logic to add a one second boundary due to the exclusive search. Add a test that reproduced the issue. --- homeassistant/components/calendar/trigger.py | 14 ++++++++---- tests/components/calendar/test_trigger.py | 24 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index 0a3171d29cc..3540a9f5148 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -81,7 +81,8 @@ class CalendarEventListener: async def _fetch_events(self, last_endtime: datetime.datetime) -> None: """Update the set of eligible events.""" - end_time = last_endtime + UPDATE_INTERVAL + # Event time ranges are exclusive so the end time is expanded by 1sec + end_time = last_endtime + UPDATE_INTERVAL + datetime.timedelta(seconds=1) _LOGGER.debug("Fetching events between %s, %s", last_endtime, end_time) events = await self._entity.async_get_events(self._hass, last_endtime, end_time) @@ -125,8 +126,12 @@ class CalendarEventListener: async def _handle_calendar_event(self, now: datetime.datetime) -> None: """Handle calendar event.""" _LOGGER.debug("Calendar event @ %s", now) + self._dispatch_events(now) + self._clear_event_listener() + self._listen_next_calendar_event() - # Consume all events that are eligible to fire + def _dispatch_events(self, now: datetime.datetime) -> None: + """Dispatch all events that are eligible to fire.""" while self._events and self._events[0][0] <= now: (_fire_time, event) = self._events.pop(0) _LOGGER.debug("Event: %s", event) @@ -134,12 +139,13 @@ class CalendarEventListener: self._job, {"trigger": {**self._trigger_data, "calendar_event": event.as_dict()}}, ) - self._clear_event_listener() - self._listen_next_calendar_event() async def _handle_refresh(self, now: datetime.datetime) -> None: """Handle core config update.""" _LOGGER.debug("Refresh events @ %s", now) + # Dispatch any eligible events in the boundary case where refresh + # fires before the calendar event. + self._dispatch_events(now) self._clear_event_listener() await self._fetch_events(now) self._listen_next_calendar_event() diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 16552f3bd61..65655bc283c 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -481,3 +481,27 @@ async def test_event_payload(hass, calls, fake_schedule): "calendar_event": event_data, } ] + + +async def test_trigger_timestamp_window_edge(hass, calls, fake_schedule, freezer): + """Test that events in the edge of a scan are included.""" + freezer.move_to("2022-04-19 11:00:00+00:00") + # Exactly at a TEST_UPDATE_INTERVAL boundary the start time, + # making this excluded from the first window. + event_data = fake_schedule.create_event( + start=datetime.datetime.fromisoformat("2022-04-19 11:14:00+00:00"), + end=datetime.datetime.fromisoformat("2022-04-19 11:30:00+00:00"), + ) + await create_automation(hass, EVENT_START) + assert len(calls()) == 0 + + await fake_schedule.fire_until( + datetime.datetime.fromisoformat("2022-04-19 11:20:00+00:00") + ) + assert calls() == [ + { + "platform": "calendar", + "event": EVENT_START, + "calendar_event": event_data, + } + ]