1 """Support for Google Calendar Search binary sensors."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
7 from datetime
import datetime, timedelta
9 from typing
import Any, cast
11 from gcal_sync.api
import Range, SyncEventsRequest
12 from gcal_sync.exceptions
import ApiException
13 from gcal_sync.model
import (
21 from gcal_sync.store
import ScopedCalendarStore
22 from gcal_sync.sync
import CalendarEventSyncManager
34 CalendarEntityDescription,
35 CalendarEntityFeature,
51 CONF_IGNORE_AVAILABILITY,
61 from .api
import get_feature_access
74 from .coordinator
import CalendarQueryUpdateCoordinator, CalendarSyncUpdateCoordinator
76 _LOGGER = logging.getLogger(__name__)
89 RRULE_PREFIX =
"RRULE:"
91 SERVICE_CREATE_EVENT =
"create_event"
94 @dataclasses.dataclass(frozen=True, kw_only=True)
96 """Google calendar entity description."""
101 ignore_availability: bool
106 working_location: bool =
False
111 config_entry: ConfigEntry,
112 calendar_item: Calendar,
113 calendar_info: Mapping[str, Any],
114 ) -> list[GoogleCalendarEntityDescription]:
115 """Create entity descriptions for the calendar.
117 The entity descriptions are based on the type of Calendar from the API
118 and optional calendar_info yaml configuration that is the older way to
119 configure calendars before they supported UI based config.
121 The yaml config may map one calendar to multiple entities and they do not
122 have a unique id. The yaml config also supports additional options like
125 calendar_id = calendar_item.id
126 num_entities = len(calendar_info[CONF_ENTITIES])
127 entity_descriptions = []
128 for data
in calendar_info[CONF_ENTITIES]:
133 entity_enabled = data.get(CONF_TRACK,
True)
134 if not entity_enabled:
136 "The 'track' option in google_calendars.yaml has been deprecated."
137 " The setting has been imported to the UI, and should now be"
138 " removed from google_calendars.yaml"
141 calendar_item.access_role.is_writer
149 search := data.get(CONF_SEARCH)
150 )
or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
155 name=data[CONF_NAME].capitalize(),
157 ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass
160 ignore_availability=data.get(CONF_IGNORE_AVAILABILITY,
False),
161 offset=data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET),
163 local_sync=local_sync,
164 entity_registry_enabled_default=entity_enabled,
165 device_id=data[CONF_DEVICE_ID],
167 entity_descriptions.append(entity_description)
169 "calendar_item.primary=%s, search=%s, calendar_item.access_role=%s - %s",
170 calendar_item.primary,
172 calendar_item.access_role,
175 if calendar_item.primary
and local_sync:
176 _LOGGER.debug(
"work location entity")
178 entity_descriptions.append(
181 key=f
"{key}-work-location",
182 translation_key=
"working_location",
183 working_location=
True,
186 entity_registry_enabled_default=
False,
189 return entity_descriptions
194 config_entry: ConfigEntry,
195 async_add_entities: AddEntitiesCallback,
197 """Set up the google calendar platform."""
198 calendar_service = hass.data[DOMAIN][config_entry.entry_id][DATA_SERVICE]
199 store = hass.data[DOMAIN][config_entry.entry_id][DATA_STORE]
201 result = await calendar_service.async_list_calendars()
202 except ApiException
as err:
205 entity_registry = er.async_get(hass)
206 registry_entries = er.async_entries_for_config_entry(
207 entity_registry, config_entry.entry_id
210 entity_entry.unique_id: entity_entry
for entity_entry
in registry_entries
214 calendars = await hass.async_add_executor_job(
215 load_config, hass.config.path(YAML_DEVICES)
219 for calendar_item
in result.items:
220 calendar_id = calendar_item.id
221 if calendars
and calendar_id
in calendars:
222 calendar_info = calendars[calendar_id]
225 hass, calendar_item.dict(exclude_unset=
True)
227 new_calendars.append(calendar_info)
230 hass, config_entry, calendar_item, calendar_info
233 f
"{config_entry.unique_id}-{entity_description.key}"
234 if entity_description.key
239 for old_unique_id
in (
241 f
"{calendar_id}-{entity_description.device_id}",
243 if not (entity_entry := entity_entry_map.get(old_unique_id)):
247 "Migrating unique_id for %s from %s to %s",
248 entity_entry.entity_id,
252 entity_registry.async_update_entity(
253 entity_entry.entity_id, new_unique_id=unique_id
257 "Removing entity registry entry for %s from %s",
258 entity_entry.entity_id,
261 entity_registry.async_remove(
262 entity_entry.entity_id,
264 _LOGGER.debug(
"Creating entity with unique_id=%s", unique_id)
265 coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator
266 if not entity_description.local_sync:
270 entity_description.name
or entity_description.key,
272 entity_description.search,
275 request_template = SyncEventsRequest(
276 calendar_id=calendar_id,
277 start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
279 sync = CalendarEventSyncManager(
281 store=ScopedCalendarStore(
282 store, unique_id
or entity_description.device_id
284 request_template=request_template,
289 entity_description.name
or entity_description.key,
302 if calendars
and new_calendars:
304 def append_calendars_to_config() -> None:
305 path = hass.config.path(YAML_DEVICES)
306 for calendar
in new_calendars:
309 await hass.async_add_executor_job(append_calendars_to_config)
311 platform = entity_platform.async_get_current_platform()
313 any(calendar_item.access_role.is_writer
for calendar_item
in result.items)
316 platform.async_register_entity_service(
317 SERVICE_CREATE_EVENT,
320 required_features=CalendarEntityFeature.CREATE_EVENT,
325 CoordinatorEntity[CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator],
328 """A calendar event entity."""
330 entity_description: GoogleCalendarEntityDescription
331 _attr_has_entity_name =
True
335 coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator,
337 entity_description: GoogleCalendarEntityDescription,
338 unique_id: str |
None,
340 """Create the Calendar event device."""
342 _LOGGER.debug(
"entity_description.entity_id=%s", entity_description.entity_id)
343 _LOGGER.debug(
"entity_description=%s", entity_description)
347 self.
_offset_offset = entity_description.offset
348 self._event: CalendarEvent |
None =
None
349 if entity_description.entity_id:
352 if not entity_description.read_only:
354 CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT
359 """Return the device state attributes."""
364 """Return whether or not the event offset was reached."""
366 if event
is not None and offset_value
is not None:
371 def event(self) -> CalendarEvent | None:
372 """Return the next upcoming event."""
377 """Return True if the event is visible and not declined."""
380 attendee.is_self
and attendee.response_status == ResponseStatus.DECLINED
381 for attendee
in event.attendees
385 if event.event_type == EventTypeEnum.WORKING_LOCATION:
389 return event.transparency == OPAQUE
392 """When entity is added to hass."""
398 self.coordinator.config_entry.async_create_background_task(
401 "google.calendar-refresh",
405 self, hass: HomeAssistant, start_date: datetime, end_date: datetime
406 ) -> list[CalendarEvent]:
407 """Get all events in a specific time frame."""
408 result_items = await self.coordinator.
async_get_events(start_date, end_date)
411 for event
in filter(self.
_event_filter_event_filter, result_items)
416 ) -> tuple[CalendarEvent | None, timedelta | None]:
417 """Get the calendar event and offset if any."""
418 if api_event := next(
421 self.coordinator.upcoming
or [],
428 event.summary, self.
_offset_offset
430 return event, offset_value
434 """Add a new event to calendar."""
435 dtstart = kwargs[EVENT_START]
436 dtend = kwargs[EVENT_END]
437 start: DateOrDatetime
439 if isinstance(dtstart, datetime):
440 start = DateOrDatetime(
441 date_time=dt_util.as_local(dtstart),
442 timezone=
str(dt_util.get_default_time_zone()),
444 end = DateOrDatetime(
445 date_time=dt_util.as_local(dtend),
446 timezone=
str(dt_util.get_default_time_zone()),
449 start = DateOrDatetime(date=dtstart)
450 end = DateOrDatetime(date=dtend)
451 event = Event.parse_obj(
453 EVENT_SUMMARY: kwargs[EVENT_SUMMARY],
456 EVENT_DESCRIPTION: kwargs.get(EVENT_DESCRIPTION),
459 if location := kwargs.get(EVENT_LOCATION):
460 event.location = location
461 if rrule := kwargs.get(EVENT_RRULE):
462 event.recurrence = [f
"{RRULE_PREFIX}{rrule}"]
466 CalendarSyncUpdateCoordinator, self.coordinator
467 ).sync.store_service.async_add_event(event)
468 except ApiException
as err:
475 recurrence_id: str |
None =
None,
476 recurrence_range: str |
None =
None,
478 """Delete an event on the calendar."""
479 range_value: Range = Range.NONE
480 if recurrence_range == Range.THIS_AND_FUTURE:
481 range_value = Range.THIS_AND_FUTURE
483 CalendarSyncUpdateCoordinator, self.coordinator
484 ).sync.store_service.async_delete_event(
486 event_id=recurrence_id,
487 recurrence_range=range_value,
493 """Return a CalendarEvent from an API event."""
494 rrule: str |
None =
None
497 len(event.recurrence) == 1
498 and (raw_rule := event.recurrence[0])
499 and raw_rule.startswith(RRULE_PREFIX)
501 rrule = raw_rule.removeprefix(RRULE_PREFIX)
504 recurrence_id=event.id
if event.recurring_event_id
else None,
506 summary=event.summary,
507 start=event.start.value,
509 description=event.description,
510 location=event.location,
515 """Add a new event to calendar."""
516 start: DateOrDatetime |
None =
None
517 end: DateOrDatetime |
None =
None
520 if EVENT_IN
in call.data:
521 if EVENT_IN_DAYS
in call.data[EVENT_IN]:
524 start_in = now +
timedelta(days=call.data[EVENT_IN][EVENT_IN_DAYS])
527 start = DateOrDatetime(date=start_in)
528 end = DateOrDatetime(date=end_in)
530 elif EVENT_IN_WEEKS
in call.data[EVENT_IN]:
533 start_in = now +
timedelta(weeks=call.data[EVENT_IN][EVENT_IN_WEEKS])
536 start = DateOrDatetime(date=start_in)
537 end = DateOrDatetime(date=end_in)
539 elif EVENT_START_DATE
in call.data
and EVENT_END_DATE
in call.data:
540 start = DateOrDatetime(date=call.data[EVENT_START_DATE])
541 end = DateOrDatetime(date=call.data[EVENT_END_DATE])
543 elif EVENT_START_DATETIME
in call.data
and EVENT_END_DATETIME
in call.data:
544 start_dt = call.data[EVENT_START_DATETIME]
545 end_dt = call.data[EVENT_END_DATETIME]
546 start = DateOrDatetime(date_time=start_dt, timezone=
str(hass.config.time_zone))
547 end = DateOrDatetime(date_time=end_dt, timezone=
str(hass.config.time_zone))
549 if start
is None or end
is None:
550 raise ValueError(
"Missing required fields to set start or end date/datetime")
553 summary=call.data[EVENT_SUMMARY],
554 description=call.data[EVENT_DESCRIPTION],
558 if location := call.data.get(EVENT_LOCATION):
559 event.location = location
562 CalendarSyncUpdateCoordinator, entity.coordinator
563 ).sync.api.async_create_event(
567 except ApiException
as err:
569 entity.async_write_ha_state()
None async_delete_event(self, str uid, str|None recurrence_id=None, str|None recurrence_range=None)
bool _event_filter(self, Event event)
CalendarEvent|None event(self)
list[CalendarEvent] async_get_events(self, HomeAssistant hass, datetime start_date, datetime end_date)
dict[str, bool] extra_state_attributes(self)
None async_create_event(self, **Any kwargs)
tuple[CalendarEvent|None, timedelta|None] _event_with_offset(self)
bool offset_reached(self)
None __init__(self, CalendarSyncUpdateCoordinator|CalendarQueryUpdateCoordinator coordinator, str calendar_id, GoogleCalendarEntityDescription entity_description, str|None unique_id)
None async_added_to_hass(self)
None async_request_refresh(self)
tuple[str, datetime.timedelta] extract_offset(str summary, str offset_prefix)
bool is_offset_reached(datetime.datetime start, datetime.timedelta offset_time)
FeatureAccess get_feature_access(ConfigEntry config_entry)
None async_create_event(GoogleCalendarEntity entity, ServiceCall call)
CalendarEvent _get_calendar_event(Event event)
list[GoogleCalendarEntityDescription] _get_entity_descriptions(HomeAssistant hass, ConfigEntry config_entry, Calendar calendar_item, Mapping[str, Any] calendar_info)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
dict[str, Any] get_calendar_info(HomeAssistant hass, Mapping[str, Any] calendar)
None update_config(str path, dict[str, Any] calendar)
str generate_entity_id(str entity_id_format, str|None name, list[str]|None current_ids=None, HomeAssistant|None hass=None)