Fix calendar trigger to survive config entry reloads (#111334)
* Fix calendar trigger to survive config entry reloads * Apply suggestions from code review --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/111791/head
parent
59b7f8d103
commit
1eac7bcbec
|
@ -91,11 +91,24 @@ EventFetcher = Callable[[Timespan], Awaitable[list[CalendarEvent]]]
|
|||
QueuedEventFetcher = Callable[[Timespan], Awaitable[list[QueuedCalendarEvent]]]
|
||||
|
||||
|
||||
def event_fetcher(hass: HomeAssistant, entity: CalendarEntity) -> EventFetcher:
|
||||
def get_entity(hass: HomeAssistant, entity_id: str) -> CalendarEntity:
|
||||
"""Get the calendar entity for the provided entity_id."""
|
||||
component: EntityComponent[CalendarEntity] = 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"
|
||||
)
|
||||
return entity
|
||||
|
||||
|
||||
def event_fetcher(hass: HomeAssistant, entity_id: str) -> EventFetcher:
|
||||
"""Build an async_get_events wrapper to fetch events during a time span."""
|
||||
|
||||
async def async_get_events(timespan: Timespan) -> list[CalendarEvent]:
|
||||
"""Return events active in the specified time span."""
|
||||
entity = get_entity(hass, entity_id)
|
||||
# Expand by one second to make the end time exclusive
|
||||
end_time = timespan.end + datetime.timedelta(seconds=1)
|
||||
return await entity.async_get_events(hass, timespan.start, end_time)
|
||||
|
@ -237,7 +250,10 @@ class CalendarEventListener:
|
|||
self._dispatch_events(now)
|
||||
self._clear_event_listener()
|
||||
self._timespan = self._timespan.next_upcoming(now, UPDATE_INTERVAL)
|
||||
self._events.extend(await self._fetcher(self._timespan))
|
||||
try:
|
||||
self._events.extend(await self._fetcher(self._timespan))
|
||||
except HomeAssistantError as ex:
|
||||
_LOGGER.error("Calendar trigger failed to fetch events: %s", ex)
|
||||
self._listen_next_calendar_event()
|
||||
|
||||
|
||||
|
@ -252,13 +268,8 @@ async def async_attach_trigger(
|
|||
event_type = config[CONF_EVENT]
|
||||
offset = config[CONF_OFFSET]
|
||||
|
||||
component: EntityComponent[CalendarEntity] = 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"
|
||||
)
|
||||
# Validate the entity id is valid
|
||||
get_entity(hass, entity_id)
|
||||
|
||||
trigger_data = {
|
||||
**trigger_info["trigger_data"],
|
||||
|
@ -270,7 +281,7 @@ async def async_attach_trigger(
|
|||
hass,
|
||||
HassJob(action),
|
||||
trigger_data,
|
||||
queued_event_fetcher(event_fetcher(hass, entity), event_type, offset),
|
||||
queued_event_fetcher(event_fetcher(hass, entity_id), event_type, offset),
|
||||
)
|
||||
await listener.async_attach()
|
||||
return listener.async_detach
|
||||
|
|
|
@ -99,8 +99,20 @@ def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]:
|
|||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
async def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Create a mock config entry."""
|
||||
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
return config_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_integration(hass: HomeAssistant, config_flow_fixture: None) -> None:
|
||||
def mock_setup_integration(
|
||||
hass: HomeAssistant,
|
||||
config_flow_fixture: None,
|
||||
test_entities: list[CalendarEntity],
|
||||
) -> None:
|
||||
"""Fixture to set up a mock integration."""
|
||||
|
||||
async def async_setup_entry_init(
|
||||
|
@ -129,20 +141,16 @@ def mock_setup_integration(hass: HomeAssistant, config_flow_fixture: None) -> No
|
|||
),
|
||||
)
|
||||
|
||||
|
||||
async def create_mock_platform(
|
||||
hass: HomeAssistant,
|
||||
entities: list[CalendarEntity],
|
||||
) -> MockConfigEntry:
|
||||
"""Create a calendar platform with the specified entities."""
|
||||
|
||||
async def async_setup_entry_platform(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up test event platform via config entry."""
|
||||
async_add_entities(entities)
|
||||
new_entities = create_test_entities()
|
||||
test_entities.clear()
|
||||
test_entities.extend(new_entities)
|
||||
async_add_entities(test_entities)
|
||||
|
||||
mock_platform(
|
||||
hass,
|
||||
|
@ -150,17 +158,15 @@ async def create_mock_platform(
|
|||
MockPlatform(async_setup_entry=async_setup_entry_platform),
|
||||
)
|
||||
|
||||
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
||||
|
||||
|
||||
@pytest.fixture(name="test_entities")
|
||||
def mock_test_entities() -> list[MockCalendarEntity]:
|
||||
"""Fixture to create fake entities used in the test."""
|
||||
"""Fixture that holdes the fake entities created during the test."""
|
||||
return []
|
||||
|
||||
|
||||
def create_test_entities() -> list[MockCalendarEntity]:
|
||||
"""Create test entities used during the test."""
|
||||
half_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30)
|
||||
entity1 = MockCalendarEntity(
|
||||
"Calendar 1",
|
||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
|
|||
from homeassistant.helpers.issue_registry import IssueRegistry
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .conftest import TEST_DOMAIN, MockCalendarEntity, create_mock_platform
|
||||
from .conftest import TEST_DOMAIN, MockCalendarEntity, MockConfigEntry
|
||||
|
||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||
|
||||
|
@ -51,10 +51,11 @@ async def mock_setup_platform(
|
|||
set_time_zone: Any,
|
||||
frozen_time: Any,
|
||||
mock_setup_integration: Any,
|
||||
test_entities: list[MockCalendarEntity],
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Fixture to setup platforms used in the test and fixtures are set up in the right order."""
|
||||
await create_mock_platform(hass, test_entities)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_events_http_api(
|
||||
|
|
|
@ -10,9 +10,7 @@ from homeassistant.const import ATTR_FRIENDLY_NAME
|
|||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import MockCalendarEntity, create_mock_platform
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.components.recorder.common import async_wait_recording_done
|
||||
|
||||
|
||||
|
@ -22,10 +20,11 @@ async def mock_setup_dependencies(
|
|||
hass: HomeAssistant,
|
||||
set_time_zone: Any,
|
||||
mock_setup_integration: None,
|
||||
test_entities: list[MockCalendarEntity],
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Fixture that ensures the recorder is setup in the right order."""
|
||||
await create_mock_platform(hass, test_entities)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_exclude_attributes(hass: HomeAssistant) -> None:
|
||||
|
|
|
@ -27,9 +27,9 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .conftest import MockCalendarEntity, create_mock_platform
|
||||
from .conftest import MockCalendarEntity
|
||||
|
||||
from tests.common import async_fire_time_changed, async_mock_service
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, async_mock_service
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -105,10 +105,11 @@ def mock_test_entity(test_entities: list[MockCalendarEntity]) -> MockCalendarEnt
|
|||
async def mock_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_integration: Any,
|
||||
test_entities: list[MockCalendarEntity],
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Fixture to setup platforms used in the test."""
|
||||
await create_mock_platform(hass, test_entities)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
@ -745,3 +746,65 @@ async def test_event_start_trigger_dst(
|
|||
"calendar_event": event3_data,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_config_entry_reload(
|
||||
hass: HomeAssistant,
|
||||
calls: Callable[[], list[dict[str, Any]]],
|
||||
fake_schedule: FakeSchedule,
|
||||
test_entities: list[MockCalendarEntity],
|
||||
setup_platform: None,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the a calendar trigger after a config entry reload.
|
||||
|
||||
This sets ups a config entry, sets up an automation for an entity in that
|
||||
config entry, then reloads the config entry. This reproduces a bug where
|
||||
the automation kept a reference to the specific entity which would be
|
||||
invalid after a config entry was reloaded.
|
||||
"""
|
||||
async with create_automation(hass, EVENT_START):
|
||||
assert len(calls()) == 0
|
||||
|
||||
assert await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
# Ensure the reloaded entity has events upcoming.
|
||||
test_entity = test_entities[1]
|
||||
event_data = test_entity.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 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_config_entry_unload(
|
||||
hass: HomeAssistant,
|
||||
calls: Callable[[], list[dict[str, Any]]],
|
||||
fake_schedule: FakeSchedule,
|
||||
test_entities: list[MockCalendarEntity],
|
||||
setup_platform: None,
|
||||
config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test an automation that references a calendar entity that is unloaded."""
|
||||
async with create_automation(hass, EVENT_START):
|
||||
assert len(calls()) == 0
|
||||
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
|
||||
await fake_schedule.fire_until(
|
||||
datetime.datetime.fromisoformat("2022-04-19 11:15:00+00:00"),
|
||||
)
|
||||
|
||||
assert "Entity does not exist calendar.calendar_2" in caplog.text
|
||||
|
|
Loading…
Reference in New Issue