mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 18:27:09 +00:00
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
@ -20,10 +20,12 @@ from homeassistant.components.websocket_api.connection import ActiveConnection
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
|
CALLBACK_TYPE,
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall,
|
ServiceCall,
|
||||||
ServiceResponse,
|
ServiceResponse,
|
||||||
SupportsResponse,
|
SupportsResponse,
|
||||||
|
callback,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.helpers.config_validation as cv
|
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 import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
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.template import DATE_STR_FORMAT
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
@ -478,6 +481,8 @@ def is_offset_reached(
|
|||||||
class CalendarEntity(Entity):
|
class CalendarEntity(Entity):
|
||||||
"""Base class for calendar event entities."""
|
"""Base class for calendar event entities."""
|
||||||
|
|
||||||
|
_alarm_unsubs: list[CALLBACK_TYPE] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event(self) -> CalendarEvent | None:
|
def event(self) -> CalendarEvent | None:
|
||||||
"""Return the next upcoming event."""
|
"""Return the next upcoming event."""
|
||||||
@ -513,6 +518,48 @@ class CalendarEntity(Entity):
|
|||||||
|
|
||||||
return STATE_OFF
|
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(
|
async def async_get_events(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -26,6 +26,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
|
|||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
Platform.BUTTON,
|
Platform.BUTTON,
|
||||||
Platform.CAMERA,
|
Platform.CAMERA,
|
||||||
|
Platform.CALENDAR,
|
||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
Platform.COVER,
|
Platform.COVER,
|
||||||
Platform.DATE,
|
Platform.DATE,
|
||||||
@ -54,7 +55,6 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
|||||||
Platform.MAILBOX,
|
Platform.MAILBOX,
|
||||||
Platform.NOTIFY,
|
Platform.NOTIFY,
|
||||||
Platform.IMAGE_PROCESSING,
|
Platform.IMAGE_PROCESSING,
|
||||||
Platform.CALENDAR,
|
|
||||||
Platform.DEVICE_TRACKER,
|
Platform.DEVICE_TRACKER,
|
||||||
Platform.WEATHER,
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
|
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config_entry: ConfigEntry,
|
||||||
add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Demo Calendar platform."""
|
"""Set up the Demo Calendar config entry."""
|
||||||
add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
DemoCalendar(calendar_data_future(), "Calendar 1"),
|
DemoCalendar(calendar_data_future(), "Calendar 1"),
|
||||||
DemoCalendar(calendar_data_current(), "Calendar 2"),
|
DemoCalendar(calendar_data_current(), "Calendar 2"),
|
||||||
|
@ -36,7 +36,7 @@ from homeassistant.components.calendar import (
|
|||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET
|
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.exceptions import HomeAssistantError, PlatformNotReady
|
||||||
from homeassistant.helpers import entity_platform, entity_registry as er
|
from homeassistant.helpers import entity_platform, entity_registry as er
|
||||||
from homeassistant.helpers.entity import generate_entity_id
|
from homeassistant.helpers.entity import generate_entity_id
|
||||||
@ -383,7 +383,6 @@ class GoogleCalendarEntity(
|
|||||||
self._event: CalendarEvent | None = None
|
self._event: CalendarEvent | None = None
|
||||||
self._attr_name = data[CONF_NAME].capitalize()
|
self._attr_name = data[CONF_NAME].capitalize()
|
||||||
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
||||||
self._offset_value: timedelta | None = None
|
|
||||||
self.entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
self._attr_entity_registry_enabled_default = entity_enabled
|
self._attr_entity_registry_enabled_default = entity_enabled
|
||||||
@ -392,17 +391,6 @@ class GoogleCalendarEntity(
|
|||||||
CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT
|
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
|
@property
|
||||||
def extra_state_attributes(self) -> dict[str, bool]:
|
def extra_state_attributes(self) -> dict[str, bool]:
|
||||||
"""Return the device state attributes."""
|
"""Return the device state attributes."""
|
||||||
@ -411,16 +399,16 @@ class GoogleCalendarEntity(
|
|||||||
@property
|
@property
|
||||||
def offset_reached(self) -> bool:
|
def offset_reached(self) -> bool:
|
||||||
"""Return whether or not the event offset was reached."""
|
"""Return whether or not the event offset was reached."""
|
||||||
if self._event and self._offset_value:
|
(event, offset_value) = self._event_with_offset()
|
||||||
return is_offset_reached(
|
if event is not None and offset_value is not None:
|
||||||
self._event.start_datetime_local, self._offset_value
|
return is_offset_reached(event.start_datetime_local, offset_value)
|
||||||
)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event(self) -> CalendarEvent | None:
|
def event(self) -> CalendarEvent | None:
|
||||||
"""Return the next upcoming event."""
|
"""Return the next upcoming event."""
|
||||||
return self._event
|
(event, _) = self._event_with_offset()
|
||||||
|
return event
|
||||||
|
|
||||||
def _event_filter(self, event: Event) -> bool:
|
def _event_filter(self, event: Event) -> bool:
|
||||||
"""Return True if the event is visible."""
|
"""Return True if the event is visible."""
|
||||||
@ -435,12 +423,10 @@ class GoogleCalendarEntity(
|
|||||||
# We do not ask for an update with async_add_entities()
|
# We do not ask for an update with async_add_entities()
|
||||||
# because it will update disabled entities. This is started as a
|
# because it will update disabled entities. This is started as a
|
||||||
# task to let if sync in the background without blocking startup
|
# 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.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(
|
async def async_get_events(
|
||||||
@ -453,8 +439,10 @@ class GoogleCalendarEntity(
|
|||||||
for event in filter(self._event_filter, result_items)
|
for event in filter(self._event_filter, result_items)
|
||||||
]
|
]
|
||||||
|
|
||||||
def _apply_coordinator_update(self) -> None:
|
def _event_with_offset(
|
||||||
"""Copy state from the coordinator to this entity."""
|
self,
|
||||||
|
) -> tuple[CalendarEvent | None, timedelta | None]:
|
||||||
|
"""Get the calendar event and offset if any."""
|
||||||
if api_event := next(
|
if api_event := next(
|
||||||
filter(
|
filter(
|
||||||
self._event_filter,
|
self._event_filter,
|
||||||
@ -462,27 +450,13 @@ class GoogleCalendarEntity(
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
):
|
):
|
||||||
self._event = _get_calendar_event(api_event)
|
event = _get_calendar_event(api_event)
|
||||||
(self._event.summary, self._offset_value) = extract_offset(
|
if self._offset:
|
||||||
self._event.summary, self._offset
|
(event.summary, offset_value) = extract_offset(
|
||||||
)
|
event.summary, self._offset
|
||||||
else:
|
)
|
||||||
self._event = None
|
return event, offset_value
|
||||||
|
return None, 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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def async_create_event(self, **kwargs: Any) -> None:
|
async def async_create_event(self, **kwargs: Any) -> None:
|
||||||
"""Add a new event to calendar."""
|
"""Add a new event to calendar."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user