mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
Add initial implementation of a calendar trigger (#68674)
* Add initial implementation of calendar trigger This is an initial implementation of a calendar trigger, that supports triggering on calendar start time. See architecture proposal in: https://github.com/home-assistant/architecture/discussions/700 * Address reviewer feedback * Use f-strings for all tests * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Remove logging f-strings, and move to main code * Remove mypy ignore * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update calendar triggers to use new calendar data model * Update tests/components/calendar/test_trigger.py Co-authored-by: Franck Nijhof <frenck@frenck.nl> * Rewrite tests using freezegun Rewrite tests using freezegun and improve edge case handling, and use utc consistently for all alarms. * Update homeassistant/components/calendar/trigger.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/calendar/trigger.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Increase test coverage based on pr feedback Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
7003862bd8
commit
a2c74b9786
@ -99,6 +99,20 @@ class CalendarEvent:
|
||||
"""Return true if the event is an all day event."""
|
||||
return not isinstance(self.start, datetime.datetime)
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict representation of the event."""
|
||||
data = {
|
||||
"start": self.start.isoformat(),
|
||||
"end": self.end.isoformat(),
|
||||
"summary": self.summary,
|
||||
"all_day": self.all_day,
|
||||
}
|
||||
if self.description:
|
||||
data["description"] = self.description
|
||||
if self.location:
|
||||
data["location"] = self.location
|
||||
return data
|
||||
|
||||
|
||||
def _get_datetime_local(
|
||||
dt_or_d: datetime.datetime | datetime.date,
|
||||
|
173
homeassistant/components/calendar/trigger.py
Normal file
173
homeassistant/components/calendar/trigger.py
Normal file
@ -0,0 +1,173 @@
|
||||
"""Offer calendar automation rules."""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.automation import (
|
||||
AutomationActionType,
|
||||
AutomationTriggerInfo,
|
||||
)
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_PLATFORM
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_point_in_utc_time,
|
||||
async_track_time_interval,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import DOMAIN, CalendarEntity, CalendarEvent
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENT_START = "start"
|
||||
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}),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class CalendarEventListener:
|
||||
"""Helper class to listen to calendar events."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
job: HassJob,
|
||||
trigger_data: dict[str, Any],
|
||||
entity: CalendarEntity,
|
||||
) -> None:
|
||||
"""Initialize CalendarEventListener."""
|
||||
self._hass = hass
|
||||
self._job = job
|
||||
self._trigger_data = trigger_data
|
||||
self._entity = entity
|
||||
self._unsub_event: CALLBACK_TYPE | None = None
|
||||
self._unsub_refresh: CALLBACK_TYPE | None = None
|
||||
# Upcoming set of events with their trigger time
|
||||
self._events: list[tuple[datetime.datetime, CalendarEvent]] = []
|
||||
|
||||
async def async_attach(self) -> None:
|
||||
"""Attach a calendar event listener."""
|
||||
now = dt_util.utcnow()
|
||||
await self._fetch_events(now)
|
||||
self._unsub_refresh = async_track_time_interval(
|
||||
self._hass, self._handle_refresh, UPDATE_INTERVAL
|
||||
)
|
||||
self._listen_next_calendar_event()
|
||||
|
||||
@callback
|
||||
def async_detach(self) -> None:
|
||||
"""Detach the calendar event listener."""
|
||||
self._clear_event_listener()
|
||||
if self._unsub_refresh:
|
||||
self._unsub_refresh()
|
||||
self._unsub_refresh = None
|
||||
|
||||
async def _fetch_events(self, now: 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)
|
||||
|
||||
# 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.sort(key=lambda x: x[0])
|
||||
|
||||
self._events.extend(
|
||||
[
|
||||
(trigger_time, event)
|
||||
for (trigger_time, event) in event_list
|
||||
if trigger_time > now
|
||||
]
|
||||
)
|
||||
_LOGGER.debug("Populated event list %s", self._events)
|
||||
|
||||
@callback
|
||||
def _listen_next_calendar_event(self) -> None:
|
||||
"""Set up the calendar event listener."""
|
||||
if not self._events:
|
||||
return
|
||||
|
||||
(event_datetime, _event) = self._events[0]
|
||||
_LOGGER.debug("Scheduling next event trigger @ %s", event_datetime)
|
||||
self._unsub_event = async_track_point_in_utc_time(
|
||||
self._hass,
|
||||
self._handle_calendar_event,
|
||||
event_datetime,
|
||||
)
|
||||
|
||||
def _clear_event_listener(self) -> None:
|
||||
"""Reset the event listener."""
|
||||
if self._unsub_event:
|
||||
self._unsub_event()
|
||||
self._unsub_event = None
|
||||
|
||||
async def _handle_calendar_event(self, now: datetime.datetime) -> None:
|
||||
"""Handle calendar event."""
|
||||
_LOGGER.debug("Calendar event @ %s", now)
|
||||
|
||||
# Consume 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)
|
||||
self._hass.async_run_hass_job(
|
||||
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)
|
||||
self._clear_event_listener()
|
||||
await self._fetch_events(now)
|
||||
self._listen_next_calendar_event()
|
||||
|
||||
|
||||
async def async_attach_trigger(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: AutomationActionType,
|
||||
automation_info: AutomationTriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach trigger for the specified calendar."""
|
||||
entity_id = config[CONF_ENTITY_ID]
|
||||
event_type = config[CONF_EVENT]
|
||||
|
||||
component: EntityComponent = hass.data[DOMAIN]
|
||||
if not (entity := component.get_entity(entity_id)) or not isinstance(
|
||||
entity, CalendarEntity
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
f"Entity does not exist {entity_id} or is not a calendar entity"
|
||||
)
|
||||
|
||||
trigger_data = {
|
||||
**automation_info["trigger_data"],
|
||||
"platform": DOMAIN,
|
||||
"event": event_type,
|
||||
}
|
||||
|
||||
listener = CalendarEventListener(hass, HassJob(action), trigger_data, entity)
|
||||
await listener.async_attach()
|
||||
return listener.async_detach
|
425
tests/components/calendar/test_trigger.py
Normal file
425
tests/components/calendar/test_trigger.py
Normal file
@ -0,0 +1,425 @@
|
||||
"""Tests for the calendar automation.
|
||||
|
||||
The tests create calendar based automations, set up a fake set of calendar
|
||||
events, then advance time to exercise that the automation is called. The
|
||||
tests use a fixture that mocks out events returned by the calendar entity,
|
||||
and create events using a relative time offset and then advance the clock
|
||||
forward exercising the triggers.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import datetime
|
||||
import logging
|
||||
import secrets
|
||||
from typing import Any, Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import calendar
|
||||
import homeassistant.components.automation as automation
|
||||
from homeassistant.components.calendar.trigger import EVENT_START
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from tests.common import async_fire_time_changed, async_mock_service
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CALENDAR_ENTITY_ID = "calendar.calendar_2"
|
||||
CONFIG = {calendar.DOMAIN: {"platform": "demo"}}
|
||||
|
||||
TEST_AUTOMATION_ACTION = {
|
||||
"service": "test.automation",
|
||||
"data": {
|
||||
"platform": "{{ trigger.platform }}",
|
||||
"event": "{{ trigger.event }}",
|
||||
"calendar_event": "{{ trigger.calendar_event }}",
|
||||
},
|
||||
}
|
||||
|
||||
# The trigger sets two alarms: One based on the next event and one
|
||||
# to refresh the schedule. The test advances the time an arbitrary
|
||||
# amount to trigger either type of event with a small jitter.
|
||||
TEST_TIME_ADVANCE_INTERVAL = datetime.timedelta(minutes=1)
|
||||
TEST_UPDATE_INTERVAL = datetime.timedelta(minutes=7)
|
||||
|
||||
|
||||
class FakeSchedule:
|
||||
"""Test fixture class for return events in a specific date range."""
|
||||
|
||||
def __init__(self, hass, freezer):
|
||||
"""Initiailize FakeSchedule."""
|
||||
self.hass = hass
|
||||
self.freezer = freezer
|
||||
# Map of event start time to event
|
||||
self.events: list[calendar.CalendarEvent] = []
|
||||
|
||||
def create_event(
|
||||
self,
|
||||
start: datetime.timedelta,
|
||||
end: datetime.timedelta,
|
||||
description: str = None,
|
||||
location: str = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a new fake event, used by tests."""
|
||||
event = calendar.CalendarEvent(
|
||||
start=start,
|
||||
end=end,
|
||||
summary=f"Event {secrets.token_hex(16)}", # Arbitrary unique data
|
||||
description=description,
|
||||
location=location,
|
||||
)
|
||||
self.events.append(event)
|
||||
return event.as_dict()
|
||||
|
||||
async def async_get_events(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
start_date: datetime.datetime,
|
||||
end_date: datetime.datetime,
|
||||
) -> list[calendar.CalendarEvent]:
|
||||
"""Get all events in a specific time frame, used by the demo calendar."""
|
||||
assert start_date < end_date
|
||||
values = []
|
||||
for event in self.events:
|
||||
if start_date < event.start < end_date or start_date < event.end < end_date:
|
||||
values.append(event)
|
||||
return values
|
||||
|
||||
async def fire_time(self, trigger_time: datetime.datetime) -> None:
|
||||
"""Fire an alarm and wait."""
|
||||
_LOGGER.debug(f"Firing alarm @ {trigger_time}")
|
||||
self.freezer.move_to(trigger_time)
|
||||
async_fire_time_changed(self.hass, trigger_time)
|
||||
await self.hass.async_block_till_done()
|
||||
|
||||
async def fire_until(self, end: datetime.timedelta) -> None:
|
||||
"""Simulate the passage of time by firing alarms until the time is reached."""
|
||||
while dt_util.utcnow() < end:
|
||||
self.freezer.tick(TEST_TIME_ADVANCE_INTERVAL)
|
||||
await self.fire_time(dt_util.utcnow())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_schedule(hass, freezer):
|
||||
"""Fixture that tests can use to make fake events."""
|
||||
|
||||
# Setup start time for all tests
|
||||
freezer.move_to("2022-04-19 10:31:02+00:00")
|
||||
|
||||
schedule = FakeSchedule(hass, freezer)
|
||||
with patch(
|
||||
"homeassistant.components.demo.calendar.DemoCalendar.async_get_events",
|
||||
new=schedule.async_get_events,
|
||||
):
|
||||
yield schedule
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_calendar(hass: HomeAssistant, fake_schedule: FakeSchedule) -> None:
|
||||
"""Initialize the demo calendar."""
|
||||
assert await async_setup_component(hass, calendar.DOMAIN, CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def create_automation(hass: HomeAssistant, event_type: str) -> None:
|
||||
"""Register an automation."""
|
||||
trigger_data = {
|
||||
"platform": calendar.DOMAIN,
|
||||
"entity_id": CALENDAR_ENTITY_ID,
|
||||
"event": event_type,
|
||||
}
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": trigger_data,
|
||||
"action": TEST_AUTOMATION_ACTION,
|
||||
"mode": "queued",
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def calls(hass: HomeAssistant) -> Callable[[], list]:
|
||||
"""Fixture to return payload data for automation calls."""
|
||||
service_calls = async_mock_service(hass, "test", "automation")
|
||||
|
||||
def get_trigger_data() -> list:
|
||||
return [c.data for c in service_calls]
|
||||
|
||||
return get_trigger_data
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_update_interval() -> Generator[None, None, None]:
|
||||
"""Fixture to override the update interval for refreshing events."""
|
||||
with patch(
|
||||
"homeassistant.components.calendar.trigger.UPDATE_INTERVAL",
|
||||
new=TEST_UPDATE_INTERVAL,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def test_event_start_trigger(hass, calls, fake_schedule):
|
||||
"""Test the a calendar trigger based on start 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 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:15:00+00:00")
|
||||
)
|
||||
assert calls() == [
|
||||
{
|
||||
"platform": "calendar",
|
||||
"event": EVENT_START,
|
||||
"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)
|
||||
|
||||
# No calls, at arbitrary times
|
||||
await fake_schedule.fire_until(
|
||||
datetime.datetime.fromisoformat("2022-04-19 11:00:00+00:00")
|
||||
)
|
||||
assert len(calls()) == 0
|
||||
|
||||
|
||||
async def test_multiple_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_START)
|
||||
|
||||
await fake_schedule.fire_until(
|
||||
datetime.datetime.fromisoformat("2022-04-19 11:30:00+00:00")
|
||||
)
|
||||
assert calls() == [
|
||||
{
|
||||
"platform": "calendar",
|
||||
"event": EVENT_START,
|
||||
"calendar_event": event_data1,
|
||||
},
|
||||
{
|
||||
"platform": "calendar",
|
||||
"event": EVENT_START,
|
||||
"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."""
|
||||
|
||||
event_data1 = fake_schedule.create_event(
|
||||
start=datetime.datetime.fromisoformat("2022-04-19 11:00:00+00:00"),
|
||||
end=datetime.datetime.fromisoformat("2022-04-19 11:30: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:30:00+00:00"),
|
||||
)
|
||||
await create_automation(hass, EVENT_START)
|
||||
|
||||
await fake_schedule.fire_until(
|
||||
datetime.datetime.fromisoformat("2022-04-19 11:35:00+00:00")
|
||||
)
|
||||
assert calls() == [
|
||||
{
|
||||
"platform": "calendar",
|
||||
"event": EVENT_START,
|
||||
"calendar_event": event_data1,
|
||||
},
|
||||
{
|
||||
"platform": "calendar",
|
||||
"event": EVENT_START,
|
||||
"calendar_event": event_data2,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_overlap_events(hass, calls, fake_schedule):
|
||||
"""Test that a trigger fires for events that overlap."""
|
||||
|
||||
event_data1 = fake_schedule.create_event(
|
||||
start=datetime.datetime.fromisoformat("2022-04-19 11:00:00+00:00"),
|
||||
end=datetime.datetime.fromisoformat("2022-04-19 11:30:00+00:00"),
|
||||
)
|
||||
event_data2 = fake_schedule.create_event(
|
||||
start=datetime.datetime.fromisoformat("2022-04-19 11:15:00+00:00"),
|
||||
end=datetime.datetime.fromisoformat("2022-04-19 11:45:00+00:00"),
|
||||
)
|
||||
await create_automation(hass, EVENT_START)
|
||||
|
||||
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_data1,
|
||||
},
|
||||
{
|
||||
"platform": "calendar",
|
||||
"event": EVENT_START,
|
||||
"calendar_event": event_data2,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_invalid_calendar_id(hass, caplog):
|
||||
"""Test creating a trigger with an invalid calendar id."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"action": TEST_AUTOMATION_ACTION,
|
||||
"trigger": {
|
||||
"platform": calendar.DOMAIN,
|
||||
"entity_id": "invalid-calendar-id",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "Invalid config for [automation]" in caplog.text
|
||||
|
||||
|
||||
async def test_legacy_entity_type(hass, caplog):
|
||||
"""Test creating a trigger with an invalid calendar id."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"action": TEST_AUTOMATION_ACTION,
|
||||
"trigger": {
|
||||
"platform": calendar.DOMAIN,
|
||||
"entity_id": "calendar.calendar_3",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "is not a calendar entity" in caplog.text
|
||||
|
||||
|
||||
async def test_update_next_event(hass, calls, fake_schedule):
|
||||
"""Test detection of a new event after initial trigger is setup."""
|
||||
|
||||
event_data1 = 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_START)
|
||||
|
||||
# No calls before event start
|
||||
await fake_schedule.fire_until(
|
||||
datetime.datetime.fromisoformat("2022-04-19 10:45:00+00:00")
|
||||
)
|
||||
assert len(calls()) == 0
|
||||
|
||||
# Create a new event between now and when the event fires
|
||||
event_data2 = fake_schedule.create_event(
|
||||
start=datetime.datetime.fromisoformat("2022-04-19 10:55:00+00:00"),
|
||||
end=datetime.datetime.fromisoformat("2022-04-19 11:05:00+00:00"),
|
||||
)
|
||||
|
||||
# Advance past the end of the events
|
||||
await fake_schedule.fire_until(
|
||||
datetime.datetime.fromisoformat("2022-04-19 11:30:00+00:00")
|
||||
)
|
||||
assert calls() == [
|
||||
{
|
||||
"platform": "calendar",
|
||||
"event": EVENT_START,
|
||||
"calendar_event": event_data2,
|
||||
},
|
||||
{
|
||||
"platform": "calendar",
|
||||
"event": EVENT_START,
|
||||
"calendar_event": event_data1,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_update_missed(hass, calls, fake_schedule):
|
||||
"""Test that new events are missed if they arrive outside the update interval."""
|
||||
|
||||
event_data1 = fake_schedule.create_event(
|
||||
start=datetime.datetime.fromisoformat("2022-04-19 11:00:00+00:00"),
|
||||
end=datetime.datetime.fromisoformat("2022-04-19 11:30:00+00:00"),
|
||||
)
|
||||
await create_automation(hass, EVENT_START)
|
||||
|
||||
# Events are refreshed at t+TEST_UPDATE_INTERVAL minutes. A new event is
|
||||
# added, but the next update happens after the event is already over.
|
||||
await fake_schedule.fire_until(
|
||||
datetime.datetime.fromisoformat("2022-04-19 10:38:00+00:00")
|
||||
)
|
||||
assert len(calls()) == 0
|
||||
|
||||
fake_schedule.create_event(
|
||||
start=datetime.datetime.fromisoformat("2022-04-19 10:40:00+00:00"),
|
||||
end=datetime.datetime.fromisoformat("2022-04-19 10:55:00+00:00"),
|
||||
)
|
||||
|
||||
# Only the first event is returned
|
||||
await fake_schedule.fire_until(
|
||||
datetime.datetime.fromisoformat("2022-04-19 11:05:00+00:00")
|
||||
)
|
||||
assert calls() == [
|
||||
{
|
||||
"platform": "calendar",
|
||||
"event": EVENT_START,
|
||||
"calendar_event": event_data1,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_event_payload(hass, calls, fake_schedule):
|
||||
"""Test the a calendar trigger based on start 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 11:30:00+00:00"),
|
||||
description="Description",
|
||||
location="Location",
|
||||
)
|
||||
await create_automation(hass, EVENT_START)
|
||||
assert len(calls()) == 0
|
||||
|
||||
await fake_schedule.fire_until(
|
||||
datetime.datetime.fromisoformat("2022-04-19 11:15:00+00:00")
|
||||
)
|
||||
assert calls() == [
|
||||
{
|
||||
"platform": "calendar",
|
||||
"event": EVENT_START,
|
||||
"calendar_event": event_data,
|
||||
}
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user