mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
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>
This commit is contained in:
parent
59b7f8d103
commit
1eac7bcbec
@ -91,11 +91,24 @@ EventFetcher = Callable[[Timespan], Awaitable[list[CalendarEvent]]]
|
|||||||
QueuedEventFetcher = Callable[[Timespan], Awaitable[list[QueuedCalendarEvent]]]
|
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."""
|
"""Build an async_get_events wrapper to fetch events during a time span."""
|
||||||
|
|
||||||
async def async_get_events(timespan: Timespan) -> list[CalendarEvent]:
|
async def async_get_events(timespan: Timespan) -> list[CalendarEvent]:
|
||||||
"""Return events active in the specified time span."""
|
"""Return events active in the specified time span."""
|
||||||
|
entity = get_entity(hass, entity_id)
|
||||||
# Expand by one second to make the end time exclusive
|
# Expand by one second to make the end time exclusive
|
||||||
end_time = timespan.end + datetime.timedelta(seconds=1)
|
end_time = timespan.end + datetime.timedelta(seconds=1)
|
||||||
return await entity.async_get_events(hass, timespan.start, end_time)
|
return await entity.async_get_events(hass, timespan.start, end_time)
|
||||||
@ -237,7 +250,10 @@ class CalendarEventListener:
|
|||||||
self._dispatch_events(now)
|
self._dispatch_events(now)
|
||||||
self._clear_event_listener()
|
self._clear_event_listener()
|
||||||
self._timespan = self._timespan.next_upcoming(now, UPDATE_INTERVAL)
|
self._timespan = self._timespan.next_upcoming(now, UPDATE_INTERVAL)
|
||||||
|
try:
|
||||||
self._events.extend(await self._fetcher(self._timespan))
|
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()
|
self._listen_next_calendar_event()
|
||||||
|
|
||||||
|
|
||||||
@ -252,13 +268,8 @@ async def async_attach_trigger(
|
|||||||
event_type = config[CONF_EVENT]
|
event_type = config[CONF_EVENT]
|
||||||
offset = config[CONF_OFFSET]
|
offset = config[CONF_OFFSET]
|
||||||
|
|
||||||
component: EntityComponent[CalendarEntity] = hass.data[DOMAIN]
|
# Validate the entity id is valid
|
||||||
if not (entity := component.get_entity(entity_id)) or not isinstance(
|
get_entity(hass, entity_id)
|
||||||
entity, CalendarEntity
|
|
||||||
):
|
|
||||||
raise HomeAssistantError(
|
|
||||||
f"Entity does not exist {entity_id} or is not a calendar entity"
|
|
||||||
)
|
|
||||||
|
|
||||||
trigger_data = {
|
trigger_data = {
|
||||||
**trigger_info["trigger_data"],
|
**trigger_info["trigger_data"],
|
||||||
@ -270,7 +281,7 @@ async def async_attach_trigger(
|
|||||||
hass,
|
hass,
|
||||||
HassJob(action),
|
HassJob(action),
|
||||||
trigger_data,
|
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()
|
await listener.async_attach()
|
||||||
return listener.async_detach
|
return listener.async_detach
|
||||||
|
@ -99,8 +99,20 @@ def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]:
|
|||||||
yield
|
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
|
@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."""
|
"""Fixture to set up a mock integration."""
|
||||||
|
|
||||||
async def async_setup_entry_init(
|
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(
|
async def async_setup_entry_platform(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up test event platform via config entry."""
|
"""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(
|
mock_platform(
|
||||||
hass,
|
hass,
|
||||||
@ -150,17 +158,15 @@ async def create_mock_platform(
|
|||||||
MockPlatform(async_setup_entry=async_setup_entry_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")
|
@pytest.fixture(name="test_entities")
|
||||||
def mock_test_entities() -> list[MockCalendarEntity]:
|
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)
|
half_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30)
|
||||||
entity1 = MockCalendarEntity(
|
entity1 = MockCalendarEntity(
|
||||||
"Calendar 1",
|
"Calendar 1",
|
||||||
|
@ -21,7 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.helpers.issue_registry import IssueRegistry
|
from homeassistant.helpers.issue_registry import IssueRegistry
|
||||||
import homeassistant.util.dt as dt_util
|
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
|
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||||
|
|
||||||
@ -51,10 +51,11 @@ async def mock_setup_platform(
|
|||||||
set_time_zone: Any,
|
set_time_zone: Any,
|
||||||
frozen_time: Any,
|
frozen_time: Any,
|
||||||
mock_setup_integration: Any,
|
mock_setup_integration: Any,
|
||||||
test_entities: list[MockCalendarEntity],
|
config_entry: MockConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Fixture to setup platforms used in the test and fixtures are set up in the right order."""
|
"""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(
|
async def test_events_http_api(
|
||||||
|
@ -10,9 +10,7 @@ from homeassistant.const import ATTR_FRIENDLY_NAME
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .conftest import MockCalendarEntity, create_mock_platform
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
|
||||||
from tests.components.recorder.common import async_wait_recording_done
|
from tests.components.recorder.common import async_wait_recording_done
|
||||||
|
|
||||||
|
|
||||||
@ -22,10 +20,11 @@ async def mock_setup_dependencies(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
set_time_zone: Any,
|
set_time_zone: Any,
|
||||||
mock_setup_integration: None,
|
mock_setup_integration: None,
|
||||||
test_entities: list[MockCalendarEntity],
|
config_entry: MockConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Fixture that ensures the recorder is setup in the right order."""
|
"""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:
|
async def test_exclude_attributes(hass: HomeAssistant) -> None:
|
||||||
|
@ -27,9 +27,9 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -105,10 +105,11 @@ def mock_test_entity(test_entities: list[MockCalendarEntity]) -> MockCalendarEnt
|
|||||||
async def mock_setup_platform(
|
async def mock_setup_platform(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_setup_integration: Any,
|
mock_setup_integration: Any,
|
||||||
test_entities: list[MockCalendarEntity],
|
config_entry: MockConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Fixture to setup platforms used in the test."""
|
"""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
|
@asynccontextmanager
|
||||||
@ -745,3 +746,65 @@ async def test_event_start_trigger_dst(
|
|||||||
"calendar_event": event3_data,
|
"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…
x
Reference in New Issue
Block a user