Add an entity description for Google Calendar (#125469)

This commit is contained in:
Allen Porter 2024-09-25 01:40:59 -07:00 committed by GitHub
parent 31d722f1ef
commit d6e34e0984
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -2,13 +2,15 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from typing import Any, cast from typing import Any, cast
from gcal_sync.api import Range, SyncEventsRequest from gcal_sync.api import Range, SyncEventsRequest
from gcal_sync.exceptions import ApiException from gcal_sync.exceptions import ApiException
from gcal_sync.model import AccessRole, DateOrDatetime, Event from gcal_sync.model import AccessRole, Calendar, DateOrDatetime, Event
from gcal_sync.store import ScopedCalendarStore from gcal_sync.store import ScopedCalendarStore
from gcal_sync.sync import CalendarEventSyncManager from gcal_sync.sync import CalendarEventSyncManager
@ -32,7 +34,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_O
from homeassistant.core import HomeAssistant, ServiceCall 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 EntityDescription, generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -81,6 +83,83 @@ RRULE_PREFIX = "RRULE:"
SERVICE_CREATE_EVENT = "create_event" SERVICE_CREATE_EVENT = "create_event"
@dataclass(frozen=True, kw_only=True)
class GoogleCalendarEntityDescription(EntityDescription):
"""Google calendar entity description."""
name: str
entity_id: str
read_only: bool
ignore_availability: bool
offset: str | None
search: str | None
local_sync: bool
device_id: str
def _get_entity_descriptions(
hass: HomeAssistant,
config_entry: ConfigEntry,
calendar_item: Calendar,
calendar_info: Mapping[str, Any],
) -> list[GoogleCalendarEntityDescription]:
"""Create entity descriptions for the calendar.
The entity descriptions are based on the type of Calendar from the API
and optional calendar_info yaml configuration that is the older way to
configure calendars before they supported UI based config.
The yaml config may map one calendar to multiple entities and they do not
have a unique id. The yaml config also supports additional options like
offsets or search.
"""
calendar_id = calendar_item.id
num_entities = len(calendar_info[CONF_ENTITIES])
entity_descriptions = []
for data in calendar_info[CONF_ENTITIES]:
if num_entities > 1:
key = ""
else:
key = calendar_id
entity_enabled = data.get(CONF_TRACK, True)
if not entity_enabled:
_LOGGER.warning(
"The 'track' option in google_calendars.yaml has been deprecated."
" The setting has been imported to the UI, and should now be"
" removed from google_calendars.yaml"
)
read_only = not (
calendar_item.access_role.is_writer
and get_feature_access(hass, config_entry) is FeatureAccess.read_write
)
# Prefer calendar sync down of resources when possible. However,
# sync does not work for search. Also free-busy calendars denormalize
# recurring events as individual events which is not efficient for sync
local_sync = True
if (
search := data.get(CONF_SEARCH)
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
read_only = True
local_sync = False
entity_descriptions.append(
GoogleCalendarEntityDescription(
key=key,
name=data[CONF_NAME].capitalize(),
entity_id=generate_entity_id(
ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass
),
read_only=read_only,
ignore_availability=data.get(CONF_IGNORE_AVAILABILITY, False),
offset=data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET),
search=search,
local_sync=local_sync,
entity_registry_enabled_default=entity_enabled,
device_id=data[CONF_DEVICE_ID],
)
)
return entity_descriptions
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
@ -117,30 +196,21 @@ async def async_setup_entry(
hass, calendar_item.dict(exclude_unset=True) hass, calendar_item.dict(exclude_unset=True)
) )
new_calendars.append(calendar_info) new_calendars.append(calendar_info)
# Yaml calendar config may map one calendar to multiple entities
# with extra options like offsets or search criteria. for entity_description in _get_entity_descriptions(
num_entities = len(calendar_info[CONF_ENTITIES]) hass, config_entry, calendar_item, calendar_info
for data in calendar_info[CONF_ENTITIES]: ):
entity_enabled = data.get(CONF_TRACK, True) unique_id = (
if not entity_enabled: f"{config_entry.unique_id}-{entity_description.key}"
_LOGGER.warning( if entity_description.key
"The 'track' option in google_calendars.yaml has been deprecated." else None
" The setting has been imported to the UI, and should now be" )
" removed from google_calendars.yaml"
)
entity_name = data[CONF_DEVICE_ID]
# The unique id is based on the config entry and calendar id since
# multiple accounts can have a common calendar id
# (e.g. `en.usa#holiday@group.v.calendar.google.com`).
# When using google_calendars.yaml with multiple entities for a
# single calendar, we have no way to set a unique id.
if num_entities > 1:
unique_id = None
else:
unique_id = f"{config_entry.unique_id}-{calendar_id}"
# Migrate to new unique_id format which supports # Migrate to new unique_id format which supports
# multiple config entries as of 2022.7 # multiple config entries as of 2022.7
for old_unique_id in (calendar_id, f"{calendar_id}-{entity_name}"): for old_unique_id in (
calendar_id,
f"{calendar_id}-{entity_description.device_id}",
):
if not (entity_entry := entity_entry_map.get(old_unique_id)): if not (entity_entry := entity_entry_map.get(old_unique_id)):
continue continue
if unique_id: if unique_id:
@ -163,24 +233,14 @@ async def async_setup_entry(
entity_entry.entity_id, entity_entry.entity_id,
) )
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator
# Prefer calendar sync down of resources when possible. However, if not entity_description.local_sync:
# sync does not work for search. Also free-busy calendars denormalize
# recurring events as individual events which is not efficient for sync
support_write = (
calendar_item.access_role.is_writer
and get_feature_access(hass, config_entry) is FeatureAccess.read_write
)
if (
search := data.get(CONF_SEARCH)
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
coordinator = CalendarQueryUpdateCoordinator( coordinator = CalendarQueryUpdateCoordinator(
hass, hass,
calendar_service, calendar_service,
data[CONF_NAME], entity_description.name,
calendar_id, calendar_id,
search, entity_description.search,
) )
support_write = False
else: else:
request_template = SyncEventsRequest( request_template = SyncEventsRequest(
calendar_id=calendar_id, calendar_id=calendar_id,
@ -188,23 +248,22 @@ async def async_setup_entry(
) )
sync = CalendarEventSyncManager( sync = CalendarEventSyncManager(
calendar_service, calendar_service,
store=ScopedCalendarStore(store, unique_id or entity_name), store=ScopedCalendarStore(
store, unique_id or entity_description.device_id
),
request_template=request_template, request_template=request_template,
) )
coordinator = CalendarSyncUpdateCoordinator( coordinator = CalendarSyncUpdateCoordinator(
hass, hass,
sync, sync,
data[CONF_NAME], entity_description.name,
) )
entities.append( entities.append(
GoogleCalendarEntity( GoogleCalendarEntity(
coordinator, coordinator,
calendar_id, calendar_id,
data, entity_description,
generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass),
unique_id, unique_id,
entity_enabled,
support_write,
) )
) )
@ -238,29 +297,26 @@ class GoogleCalendarEntity(
): ):
"""A calendar event entity.""" """A calendar event entity."""
entity_description: GoogleCalendarEntityDescription
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__( def __init__(
self, self,
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator, coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator,
calendar_id: str, calendar_id: str,
data: dict[str, Any], entity_description: GoogleCalendarEntityDescription,
entity_id: str,
unique_id: str | None, unique_id: str | None,
entity_enabled: bool,
supports_write: bool,
) -> None: ) -> None:
"""Create the Calendar event device.""" """Create the Calendar event device."""
super().__init__(coordinator) super().__init__(coordinator)
self.calendar_id = calendar_id self.calendar_id = calendar_id
self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False) self.entity_description = entity_description
self._ignore_availability = entity_description.ignore_availability
self._offset = entity_description.offset
self._event: CalendarEvent | None = None self._event: CalendarEvent | None = None
self._attr_name = data[CONF_NAME].capitalize() self.entity_id = entity_description.entity_id
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
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 if not entity_description.read_only:
if supports_write:
self._attr_supported_features = ( self._attr_supported_features = (
CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT
) )