Skip to content

Commit

Permalink
Add an entity descripton for Google Calendar
Browse files Browse the repository at this point in the history
  • Loading branch information
allenporter committed Sep 7, 2024
1 parent ce28d8a commit 3916d2d
Showing 1 changed file with 109 additions and 53 deletions.
162 changes: 109 additions & 53 deletions homeassistant/components/google/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
from typing import Any, cast

from gcal_sync.api import Range, SyncEventsRequest
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.sync import CalendarEventSyncManager

Expand All @@ -32,7 +34,7 @@
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
from homeassistant.helpers.entity import EntityDescription, generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
Expand Down Expand Up @@ -81,6 +83,83 @@
SERVICE_CREATE_EVENT = "create_event"


@dataclass(frozen=True)
class GoogleCalendarEntityDescription(EntityDescription):
"""Google calendar entity description."""

name: str = ""
entity_id: str = ""
read_only: bool = False
ignore_availability: bool = False
offset: str | None = None
search: str | None = None
local_sync: bool = True
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(

Check warning on line 126 in homeassistant/components/google/calendar.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/google/calendar.py#L126

Added line #L126 was not covered by tests
"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(
hass: HomeAssistant,
config_entry: ConfigEntry,
Expand Down Expand Up @@ -117,30 +196,21 @@ async def async_setup_entry(
hass, calendar_item.dict(exclude_unset=True)
)
new_calendars.append(calendar_info)
# Yaml calendar config may map one calendar to multiple entities
# with extra options like offsets or search criteria.
num_entities = len(calendar_info[CONF_ENTITIES])
for data in calendar_info[CONF_ENTITIES]:
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"
)
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#[email protected]`).
# 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}"

for entity_description in _get_entity_descriptions(
hass, config_entry, calendar_item, calendar_info
):
unique_id = (
f"{config_entry.unique_id}-{entity_description.key}"
if entity_description.key
else None
)
# Migrate to new unique_id format which supports
# 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)):
continue
if unique_id:
Expand All @@ -163,48 +233,37 @@ async def async_setup_entry(
entity_entry.entity_id,
)
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator
# 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
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:
if not entity_description.local_sync:
coordinator = CalendarQueryUpdateCoordinator(
hass,
calendar_service,
data[CONF_NAME],
entity_description.name,
calendar_id,
search,
entity_description.search,
)
support_write = False
else:
request_template = SyncEventsRequest(
calendar_id=calendar_id,
start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
)
sync = CalendarEventSyncManager(
calendar_service,
store=ScopedCalendarStore(store, unique_id or entity_name),
store=ScopedCalendarStore(
store, unique_id or entity_description.device_id
),
request_template=request_template,
)
coordinator = CalendarSyncUpdateCoordinator(
hass,
sync,
data[CONF_NAME],
entity_description.name,
)
entities.append(
GoogleCalendarEntity(
coordinator,
calendar_id,
data,
generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass),
entity_description,
unique_id,
entity_enabled,
support_write,
)
)

Expand Down Expand Up @@ -238,29 +297,26 @@ class GoogleCalendarEntity(
):
"""A calendar event entity."""

entity_description: GoogleCalendarEntityDescription
_attr_has_entity_name = True

def __init__(
self,
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator,
calendar_id: str,
data: dict[str, Any],
entity_id: str,
entity_description: GoogleCalendarEntityDescription,
unique_id: str | None,
entity_enabled: bool,
supports_write: bool,
) -> None:
"""Create the Calendar event device."""
super().__init__(coordinator)
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._attr_name = data[CONF_NAME].capitalize()
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
self.entity_id = entity_id
self.entity_id = entity_description.entity_id
self._attr_unique_id = unique_id
self._attr_entity_registry_enabled_default = entity_enabled
if supports_write:
if not entity_description.read_only:
self._attr_supported_features = (
CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT
)
Expand Down

0 comments on commit 3916d2d

Please sign in to comment.