-
-
Notifications
You must be signed in to change notification settings - Fork 31.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add an entity descripton for Google Calendar
- Loading branch information
1 parent
ce28d8a
commit 3916d2d
Showing
1 changed file
with
109 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
@@ -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 | ||
|
@@ -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( | ||
"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, | ||
|
@@ -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: | ||
|
@@ -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, | ||
) | ||
) | ||
|
||
|
@@ -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 | ||
) | ||
|