diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index b53a85de38f..0a3171d29cc 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -28,13 +28,14 @@ from . import DOMAIN, CalendarEntity, CalendarEvent _LOGGER = logging.getLogger(__name__) EVENT_START = "start" +EVENT_END = "end" UPDATE_INTERVAL = datetime.timedelta(minutes=15) TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): DOMAIN, vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Optional(CONF_EVENT, default=EVENT_START): vol.In({EVENT_START}), + vol.Optional(CONF_EVENT, default=EVENT_START): vol.In({EVENT_START, EVENT_END}), } ) @@ -48,6 +49,7 @@ class CalendarEventListener: job: HassJob, trigger_data: dict[str, Any], entity: CalendarEntity, + event_type: str, ) -> None: """Initialize CalendarEventListener.""" self._hass = hass @@ -58,6 +60,7 @@ class CalendarEventListener: self._unsub_refresh: CALLBACK_TYPE | None = None # Upcoming set of events with their trigger time self._events: list[tuple[datetime.datetime, CalendarEvent]] = [] + self._event_type = event_type async def async_attach(self) -> None: """Attach a calendar event listener.""" @@ -76,29 +79,27 @@ class CalendarEventListener: self._unsub_refresh() self._unsub_refresh = None - async def _fetch_events(self, now: datetime.datetime) -> None: + async def _fetch_events(self, last_endtime: datetime.datetime) -> None: """Update the set of eligible events.""" - start_date = now - end_date = now + UPDATE_INTERVAL - _LOGGER.debug("Fetching events between %s, %s", start_date, end_date) - events = await self._entity.async_get_events(self._hass, start_date, end_date) + end_time = last_endtime + UPDATE_INTERVAL + _LOGGER.debug("Fetching events between %s, %s", last_endtime, end_time) + events = await self._entity.async_get_events(self._hass, last_endtime, end_time) # Build list of events and the appropriate time to trigger an alarm. The # returned events may have already started but matched the start/end time # filtering above, so exclude any events that have already passed the # trigger time. - event_list = [ - (dt_util.as_utc(event.start_datetime_local), event) for event in events - ] + event_list = [] + for event in events: + event_time = ( + event.start_datetime_local + if self._event_type == EVENT_START + else event.end_datetime_local + ) + if event_time > last_endtime: + event_list.append((event_time, event)) event_list.sort(key=lambda x: x[0]) - - self._events.extend( - [ - (trigger_time, event) - for (trigger_time, event) in event_list - if trigger_time > now - ] - ) + self._events = event_list _LOGGER.debug("Populated event list %s", self._events) @callback @@ -168,6 +169,8 @@ async def async_attach_trigger( "event": event_type, } - listener = CalendarEventListener(hass, HassJob(action), trigger_data, entity) + listener = CalendarEventListener( + hass, HassJob(action), trigger_data, entity, event_type + ) await listener.async_attach() return listener.async_detach diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 61cd76fd6f8..16552f3bd61 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -19,7 +19,7 @@ import pytest from homeassistant.components import calendar import homeassistant.components.automation as automation -from homeassistant.components.calendar.trigger import EVENT_START +from homeassistant.components.calendar.trigger import EVENT_END, EVENT_START from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -189,10 +189,38 @@ async def test_event_start_trigger(hass, calls, fake_schedule): ] +async def test_event_end_trigger(hass, calls, fake_schedule): + """Test the a calendar trigger based on end time.""" + event_data = fake_schedule.create_event( + start=datetime.datetime.fromisoformat("2022-04-19 11:00:00+00:00"), + end=datetime.datetime.fromisoformat("2022-04-19 12:00:00+00:00"), + ) + await create_automation(hass, EVENT_END) + + # Event started, nothing should fire yet + await fake_schedule.fire_until( + datetime.datetime.fromisoformat("2022-04-19 11:10:00+00:00") + ) + assert len(calls()) == 0 + + # Event ends + await fake_schedule.fire_until( + datetime.datetime.fromisoformat("2022-04-19 12:10:00+00:00") + ) + assert calls() == [ + { + "platform": "calendar", + "event": EVENT_END, + "calendar_event": event_data, + } + ] + + async def test_calendar_trigger_with_no_events(hass, calls, fake_schedule): """Test a calendar trigger setup with no events.""" await create_automation(hass, EVENT_START) + await create_automation(hass, EVENT_END) # No calls, at arbitrary times await fake_schedule.fire_until( @@ -201,7 +229,7 @@ async def test_calendar_trigger_with_no_events(hass, calls, fake_schedule): assert len(calls()) == 0 -async def test_multiple_events(hass, calls, fake_schedule): +async def test_multiple_start_events(hass, calls, fake_schedule): """Test that a trigger fires for multiple events.""" event_data1 = fake_schedule.create_event( @@ -231,6 +259,36 @@ async def test_multiple_events(hass, calls, fake_schedule): ] +async def test_multiple_end_events(hass, calls, fake_schedule): + """Test that a trigger fires for multiple events.""" + + event_data1 = fake_schedule.create_event( + start=datetime.datetime.fromisoformat("2022-04-19 10:45:00+00:00"), + end=datetime.datetime.fromisoformat("2022-04-19 11:00:00+00:00"), + ) + event_data2 = fake_schedule.create_event( + start=datetime.datetime.fromisoformat("2022-04-19 11:00:00+00:00"), + end=datetime.datetime.fromisoformat("2022-04-19 11:15:00+00:00"), + ) + await create_automation(hass, EVENT_END) + + await fake_schedule.fire_until( + datetime.datetime.fromisoformat("2022-04-19 11:30:00+00:00") + ) + assert calls() == [ + { + "platform": "calendar", + "event": EVENT_END, + "calendar_event": event_data1, + }, + { + "platform": "calendar", + "event": EVENT_END, + "calendar_event": event_data2, + }, + ] + + async def test_multiple_events_sharing_start_time(hass, calls, fake_schedule): """Test that a trigger fires for every event sharing a start time."""