Update calendar handle state updates at start/end of active/upcoming event (#98037)
* Update calendar handle state updates at start/end of active/upcoming event * Use async_write_ha_state intercept state updates Remove unrelated changes and whitespace. * Revert unnecessary changes * Move demo calendar to config entries to cleanup event timers * Fix docs on calendars * Move method inside from PR feedback
This commit is contained in:
parent
544d6b05a5
commit
8d9c5a61ec
4 changed files with 75 additions and 55 deletions
|
@ -20,10 +20,12 @@ from homeassistant.components.websocket_api.connection import ActiveConnection
|
|||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -34,6 +36,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
|
|||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_point_in_time
|
||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
@ -478,6 +481,8 @@ def is_offset_reached(
|
|||
class CalendarEntity(Entity):
|
||||
"""Base class for calendar event entities."""
|
||||
|
||||
_alarm_unsubs: list[CALLBACK_TYPE] = []
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
|
@ -513,6 +518,48 @@ class CalendarEntity(Entity):
|
|||
|
||||
return STATE_OFF
|
||||
|
||||
@callback
|
||||
def async_write_ha_state(self) -> None:
|
||||
"""Write the state to the state machine.
|
||||
|
||||
This sets up listeners to handle state transitions for start or end of
|
||||
the current or upcoming event.
|
||||
"""
|
||||
super().async_write_ha_state()
|
||||
|
||||
for unsub in self._alarm_unsubs:
|
||||
unsub()
|
||||
|
||||
now = dt_util.now()
|
||||
event = self.event
|
||||
if event is None or now >= event.end_datetime_local:
|
||||
return
|
||||
|
||||
@callback
|
||||
def update(_: datetime.datetime) -> None:
|
||||
"""Run when the active or upcoming event starts or ends."""
|
||||
self._async_write_ha_state()
|
||||
|
||||
if now < event.start_datetime_local:
|
||||
self._alarm_unsubs.append(
|
||||
async_track_point_in_time(
|
||||
self.hass,
|
||||
update,
|
||||
event.start_datetime_local,
|
||||
)
|
||||
)
|
||||
self._alarm_unsubs.append(
|
||||
async_track_point_in_time(self.hass, update, event.end_datetime_local)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Run when entity will be removed from hass.
|
||||
|
||||
To be extended by integrations.
|
||||
"""
|
||||
for unsub in self._alarm_unsubs:
|
||||
unsub()
|
||||
|
||||
async def async_get_events(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
|
|
|
@ -26,6 +26,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
|
|||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CAMERA,
|
||||
Platform.CALENDAR,
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.DATE,
|
||||
|
@ -54,7 +55,6 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
|||
Platform.MAILBOX,
|
||||
Platform.NOTIFY,
|
||||
Platform.IMAGE_PROCESSING,
|
||||
Platform.CALENDAR,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.WEATHER,
|
||||
]
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
"""Demo platform that has two fake binary sensors."""
|
||||
"""Demo platform that has two fake calendars."""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
|
||||
def setup_platform(
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Demo Calendar platform."""
|
||||
add_entities(
|
||||
"""Set up the Demo Calendar config entry."""
|
||||
async_add_entities(
|
||||
[
|
||||
DemoCalendar(calendar_data_future(), "Calendar 1"),
|
||||
DemoCalendar(calendar_data_current(), "Calendar 2"),
|
||||
|
|
|
@ -36,7 +36,7 @@ from homeassistant.components.calendar import (
|
|||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||
from homeassistant.helpers import entity_platform, entity_registry as er
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
|
@ -383,7 +383,6 @@ class GoogleCalendarEntity(
|
|||
self._event: CalendarEvent | None = None
|
||||
self._attr_name = data[CONF_NAME].capitalize()
|
||||
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
||||
self._offset_value: timedelta | None = None
|
||||
self.entity_id = entity_id
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_entity_registry_enabled_default = entity_enabled
|
||||
|
@ -392,17 +391,6 @@ class GoogleCalendarEntity(
|
|||
CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT
|
||||
)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Enable polling for the entity.
|
||||
|
||||
The coordinator is not used by multiple entities, but instead
|
||||
is used to poll the calendar API at a separate interval from the
|
||||
entity state updates itself which happen more frequently (e.g. to
|
||||
fire an alarm when the next event starts).
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, bool]:
|
||||
"""Return the device state attributes."""
|
||||
|
@ -411,16 +399,16 @@ class GoogleCalendarEntity(
|
|||
@property
|
||||
def offset_reached(self) -> bool:
|
||||
"""Return whether or not the event offset was reached."""
|
||||
if self._event and self._offset_value:
|
||||
return is_offset_reached(
|
||||
self._event.start_datetime_local, self._offset_value
|
||||
)
|
||||
(event, offset_value) = self._event_with_offset()
|
||||
if event is not None and offset_value is not None:
|
||||
return is_offset_reached(event.start_datetime_local, offset_value)
|
||||
return False
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
return self._event
|
||||
(event, _) = self._event_with_offset()
|
||||
return event
|
||||
|
||||
def _event_filter(self, event: Event) -> bool:
|
||||
"""Return True if the event is visible."""
|
||||
|
@ -435,12 +423,10 @@ class GoogleCalendarEntity(
|
|||
# We do not ask for an update with async_add_entities()
|
||||
# because it will update disabled entities. This is started as a
|
||||
# task to let if sync in the background without blocking startup
|
||||
async def refresh() -> None:
|
||||
await self.coordinator.async_request_refresh()
|
||||
self._apply_coordinator_update()
|
||||
|
||||
self.coordinator.config_entry.async_create_background_task(
|
||||
self.hass, refresh(), "google.calendar-refresh"
|
||||
self.hass,
|
||||
self.coordinator.async_request_refresh(),
|
||||
"google.calendar-refresh",
|
||||
)
|
||||
|
||||
async def async_get_events(
|
||||
|
@ -453,8 +439,10 @@ class GoogleCalendarEntity(
|
|||
for event in filter(self._event_filter, result_items)
|
||||
]
|
||||
|
||||
def _apply_coordinator_update(self) -> None:
|
||||
"""Copy state from the coordinator to this entity."""
|
||||
def _event_with_offset(
|
||||
self,
|
||||
) -> tuple[CalendarEvent | None, timedelta | None]:
|
||||
"""Get the calendar event and offset if any."""
|
||||
if api_event := next(
|
||||
filter(
|
||||
self._event_filter,
|
||||
|
@ -462,27 +450,13 @@ class GoogleCalendarEntity(
|
|||
),
|
||||
None,
|
||||
):
|
||||
self._event = _get_calendar_event(api_event)
|
||||
(self._event.summary, self._offset_value) = extract_offset(
|
||||
self._event.summary, self._offset
|
||||
event = _get_calendar_event(api_event)
|
||||
if self._offset:
|
||||
(event.summary, offset_value) = extract_offset(
|
||||
event.summary, self._offset
|
||||
)
|
||||
else:
|
||||
self._event = None
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._apply_coordinator_update()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Disable update behavior.
|
||||
|
||||
This relies on the coordinator callback update to write home assistant
|
||||
state with the next calendar event. This update is a no-op as no new data
|
||||
fetch is needed to evaluate the state to determine if the next event has
|
||||
started, handled by CalendarEntity parent class.
|
||||
"""
|
||||
return event, offset_value
|
||||
return None, None
|
||||
|
||||
async def async_create_event(self, **kwargs: Any) -> None:
|
||||
"""Add a new event to calendar."""
|
||||
|
|
Loading…
Add table
Reference in a new issue