mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Fix remote calendar state after refreshing events (#175623)
This commit is contained in:
@@ -8,7 +8,7 @@ from ical.event import Event
|
||||
from ical.timeline import Timeline, materialize_timeline
|
||||
|
||||
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -88,15 +88,13 @@ class RemoteCalendarEntity(
|
||||
|
||||
return await self.hass.async_add_executor_job(events_in_range)
|
||||
|
||||
@override
|
||||
async def async_update(self) -> None:
|
||||
"""Refresh the timeline.
|
||||
async def _async_update_timeline(self) -> None:
|
||||
"""Refresh the timeline and write state.
|
||||
|
||||
This is called when the coordinator updates. Creating the timeline may
|
||||
require walking through the entire calendar and handling recurring
|
||||
events, so it is done as a separate task without blocking the event loop.
|
||||
"""
|
||||
await super().async_update()
|
||||
|
||||
def _get_timeline() -> Timeline | None:
|
||||
"""Return a materialized timeline with upcoming events."""
|
||||
@@ -111,6 +109,28 @@ class RemoteCalendarEntity(
|
||||
|
||||
self._timeline = await self.hass.async_add_executor_job(_get_timeline)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self._async_update_timeline()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
@override
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self.coordinator.config_entry.async_create_task(
|
||||
self.hass,
|
||||
self._async_handle_coordinator_update(),
|
||||
name="remote calendar timeline update",
|
||||
)
|
||||
|
||||
async def _async_handle_coordinator_update(self) -> None:
|
||||
"""Refresh the timeline and write state."""
|
||||
await self._async_update_timeline()
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
def _get_calendar_event(event: Event) -> CalendarEvent:
|
||||
"""Return a CalendarEvent from an API event."""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Tests for calendar platform of Remote Calendar."""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import pathlib
|
||||
import textwrap
|
||||
|
||||
@@ -12,6 +12,7 @@ from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import setup_integration
|
||||
from .conftest import (
|
||||
@@ -530,3 +531,59 @@ async def test_event_edge_during_refresh_interval(
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("message") == "Event Two"
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.freeze_time("2026-05-18 06:00:00+00:00")
|
||||
async def test_coordinator_refresh_updates_upcoming_event_state(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test a coordinator refresh updates the materialized upcoming event."""
|
||||
original_calendar = textwrap.dedent(
|
||||
"""\
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:Wake up
|
||||
DTSTART:20260518T064000
|
||||
DTEND:20260518T065500
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
"""
|
||||
)
|
||||
updated_calendar = textwrap.dedent(
|
||||
"""\
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:Wake up
|
||||
DTSTART:20260519T080000
|
||||
DTEND:20260519T081500
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
"""
|
||||
)
|
||||
route = respx.get(CALENDER_URL).mock(
|
||||
side_effect=[
|
||||
Response(status_code=200, text=original_calendar),
|
||||
# We currently update the calendar twice on startup, tracked
|
||||
# in issue #148315
|
||||
Response(status_code=200, text=original_calendar),
|
||||
Response(status_code=200, text=updated_calendar),
|
||||
]
|
||||
)
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
state = hass.states.get(TEST_ENTITY)
|
||||
assert state
|
||||
assert state.attributes.get("start_time") == "2026-05-18 06:40:00"
|
||||
|
||||
# Advance clock to trigger the next update interval
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_ENTITY)
|
||||
assert state
|
||||
assert state.attributes.get("start_time") == "2026-05-19 08:00:00"
|
||||
assert route.call_count == 3
|
||||
|
||||
Reference in New Issue
Block a user