2019-02-14 15:01:46 +00:00
|
|
|
"""Support for Google Calendar Search binary sensors."""
|
2022-05-22 21:29:11 +00:00
|
|
|
|
2022-01-03 12:14:02 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2019-07-11 03:59:37 +00:00
|
|
|
import copy
|
2022-04-21 03:18:24 +00:00
|
|
|
from datetime import datetime, timedelta
|
2019-03-21 05:56:46 +00:00
|
|
|
import logging
|
2022-02-01 16:28:32 +00:00
|
|
|
from typing import Any
|
2017-05-19 14:39:13 +00:00
|
|
|
|
2022-04-21 03:18:24 +00:00
|
|
|
from gcal_sync.api import GoogleCalendarService, ListEventsRequest
|
|
|
|
from gcal_sync.exceptions import ApiException
|
|
|
|
from gcal_sync.model import Event
|
2019-10-18 00:11:51 +00:00
|
|
|
|
2019-07-11 03:59:37 +00:00
|
|
|
from homeassistant.components.calendar import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ENTITY_ID_FORMAT,
|
2022-04-10 19:04:07 +00:00
|
|
|
CalendarEntity,
|
|
|
|
CalendarEvent,
|
2022-03-27 17:02:19 +00:00
|
|
|
extract_offset,
|
2019-07-31 19:25:30 +00:00
|
|
|
is_offset_reached,
|
|
|
|
)
|
2022-03-15 06:51:02 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2021-02-08 11:24:48 +00:00
|
|
|
from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET
|
2022-03-15 06:51:02 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
|
|
|
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
2019-07-11 03:59:37 +00:00
|
|
|
from homeassistant.helpers.entity import generate_entity_id
|
2022-01-03 12:14:02 +00:00
|
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
2022-04-21 03:18:24 +00:00
|
|
|
from homeassistant.util import Throttle
|
2017-05-19 14:39:13 +00:00
|
|
|
|
2019-03-21 05:56:46 +00:00
|
|
|
from . import (
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_CAL_ID,
|
|
|
|
CONF_IGNORE_AVAILABILITY,
|
|
|
|
CONF_SEARCH,
|
|
|
|
CONF_TRACK,
|
2022-02-27 00:19:45 +00:00
|
|
|
DATA_SERVICE,
|
2019-07-31 19:25:30 +00:00
|
|
|
DEFAULT_CONF_OFFSET,
|
2022-02-27 00:19:45 +00:00
|
|
|
DOMAIN,
|
2022-03-15 06:51:02 +00:00
|
|
|
SERVICE_SCAN_CALENDARS,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2022-03-15 06:51:02 +00:00
|
|
|
from .const import DISCOVER_CALENDAR
|
2019-03-21 05:56:46 +00:00
|
|
|
|
2017-05-19 14:39:13 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
DEFAULT_GOOGLE_SEARCH_PARAMS = {
|
2019-07-31 19:25:30 +00:00
|
|
|
"orderBy": "startTime",
|
|
|
|
"singleEvents": True,
|
2017-05-19 14:39:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
|
|
|
|
2022-02-26 23:27:08 +00:00
|
|
|
# Events have a transparency that determine whether or not they block time on calendar.
|
|
|
|
# When an event is opaque, it means "Show me as busy" which is the default. Events that
|
|
|
|
# are not opaque are ignored by default.
|
|
|
|
TRANSPARENCY = "transparency"
|
|
|
|
OPAQUE = "opaque"
|
|
|
|
|
2017-05-19 14:39:13 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
async def async_setup_entry(
|
|
|
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
2022-01-03 12:14:02 +00:00
|
|
|
) -> None:
|
2022-03-15 06:51:02 +00:00
|
|
|
"""Set up the google calendar platform."""
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_discover(discovery_info: dict[str, Any]) -> None:
|
|
|
|
_async_setup_entities(
|
|
|
|
hass,
|
|
|
|
entry,
|
|
|
|
async_add_entities,
|
|
|
|
discovery_info,
|
|
|
|
)
|
|
|
|
|
|
|
|
entry.async_on_unload(
|
|
|
|
async_dispatcher_connect(hass, DISCOVER_CALENDAR, async_discover)
|
|
|
|
)
|
2017-05-19 14:39:13 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
# Look for any new calendars
|
|
|
|
try:
|
|
|
|
await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, blocking=True)
|
|
|
|
except HomeAssistantError as err:
|
|
|
|
# This can happen if there's a connection error during setup.
|
|
|
|
raise PlatformNotReady(str(err)) from err
|
2017-05-19 14:39:13 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_setup_entities(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
entry: ConfigEntry,
|
|
|
|
async_add_entities: AddEntitiesCallback,
|
|
|
|
disc_info: dict[str, Any],
|
|
|
|
) -> None:
|
2022-02-27 00:19:45 +00:00
|
|
|
calendar_service = hass.data[DOMAIN][DATA_SERVICE]
|
2019-07-11 03:59:37 +00:00
|
|
|
entities = []
|
2022-05-22 21:29:11 +00:00
|
|
|
num_entities = len(disc_info[CONF_ENTITIES])
|
2019-07-11 03:59:37 +00:00
|
|
|
for data in disc_info[CONF_ENTITIES]:
|
2022-05-22 21:29:11 +00:00
|
|
|
entity_enabled = data.get(CONF_TRACK, True)
|
2022-05-27 09:52:24 +00:00
|
|
|
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"
|
|
|
|
)
|
2022-05-22 21:29:11 +00:00
|
|
|
entity_name = data[CONF_DEVICE_ID]
|
|
|
|
entity_id = generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass)
|
|
|
|
calendar_id = disc_info[CONF_CAL_ID]
|
|
|
|
if num_entities > 1:
|
|
|
|
# The google_calendars.yaml file lets users add multiple entities for
|
|
|
|
# the same calendar id and needs additional disambiguation
|
|
|
|
unique_id = f"{calendar_id}-{entity_name}"
|
|
|
|
else:
|
|
|
|
unique_id = calendar_id
|
2022-04-10 19:04:07 +00:00
|
|
|
entity = GoogleCalendarEntity(
|
2022-05-22 21:29:11 +00:00
|
|
|
calendar_service,
|
|
|
|
disc_info[CONF_CAL_ID],
|
|
|
|
data,
|
|
|
|
entity_id,
|
|
|
|
unique_id,
|
|
|
|
entity_enabled,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-07-11 03:59:37 +00:00
|
|
|
entities.append(entity)
|
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
async_add_entities(entities, True)
|
2017-05-19 14:39:13 +00:00
|
|
|
|
|
|
|
|
2022-04-10 19:04:07 +00:00
|
|
|
class GoogleCalendarEntity(CalendarEntity):
|
2017-05-19 14:39:13 +00:00
|
|
|
"""A calendar event device."""
|
|
|
|
|
2022-02-01 16:28:32 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
calendar_service: GoogleCalendarService,
|
|
|
|
calendar_id: str,
|
|
|
|
data: dict[str, Any],
|
|
|
|
entity_id: str,
|
2022-05-22 21:29:11 +00:00
|
|
|
unique_id: str,
|
|
|
|
entity_enabled: bool,
|
2022-02-01 16:28:32 +00:00
|
|
|
) -> None:
|
2017-05-19 14:39:13 +00:00
|
|
|
"""Create the Calendar event device."""
|
2022-02-27 23:01:54 +00:00
|
|
|
self._calendar_service = calendar_service
|
|
|
|
self._calendar_id = calendar_id
|
|
|
|
self._search: str | None = data.get(CONF_SEARCH)
|
|
|
|
self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False)
|
2022-04-10 19:04:07 +00:00
|
|
|
self._event: CalendarEvent | None = None
|
2022-02-01 16:28:32 +00:00
|
|
|
self._name: str = data[CONF_NAME]
|
2019-07-11 03:59:37 +00:00
|
|
|
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
2022-04-14 02:04:59 +00:00
|
|
|
self._offset_value: timedelta | None = None
|
2019-07-11 03:59:37 +00:00
|
|
|
self.entity_id = entity_id
|
2022-05-22 21:29:11 +00:00
|
|
|
self._attr_unique_id = unique_id
|
|
|
|
self._attr_entity_registry_enabled_default = entity_enabled
|
2019-07-11 03:59:37 +00:00
|
|
|
|
|
|
|
@property
|
2022-02-01 16:28:32 +00:00
|
|
|
def extra_state_attributes(self) -> dict[str, bool]:
|
2019-07-11 03:59:37 +00:00
|
|
|
"""Return the device state attributes."""
|
2022-04-14 02:04:59 +00:00
|
|
|
return {"offset_reached": self.offset_reached}
|
|
|
|
|
|
|
|
@property
|
|
|
|
def offset_reached(self) -> bool:
|
|
|
|
"""Return whether or not the event offset was reached."""
|
|
|
|
if self._event and self._offset_value:
|
|
|
|
return is_offset_reached(
|
|
|
|
self._event.start_datetime_local, self._offset_value
|
|
|
|
)
|
|
|
|
return False
|
2019-07-11 03:59:37 +00:00
|
|
|
|
|
|
|
@property
|
2022-04-10 19:04:07 +00:00
|
|
|
def event(self) -> CalendarEvent | None:
|
2019-07-11 03:59:37 +00:00
|
|
|
"""Return the next upcoming event."""
|
|
|
|
return self._event
|
|
|
|
|
|
|
|
@property
|
2022-02-01 16:28:32 +00:00
|
|
|
def name(self) -> str:
|
2019-07-11 03:59:37 +00:00
|
|
|
"""Return the name of the entity."""
|
|
|
|
return self._name
|
2017-05-19 14:39:13 +00:00
|
|
|
|
2022-04-21 03:18:24 +00:00
|
|
|
def _event_filter(self, event: Event) -> bool:
|
2022-02-26 23:27:08 +00:00
|
|
|
"""Return True if the event is visible."""
|
2022-02-27 23:01:54 +00:00
|
|
|
if self._ignore_availability:
|
2022-02-26 23:27:08 +00:00
|
|
|
return True
|
2022-05-22 21:29:11 +00:00
|
|
|
return event.transparency == OPAQUE # type: ignore[no-any-return]
|
2022-02-26 23:27:08 +00:00
|
|
|
|
2022-02-01 16:28:32 +00:00
|
|
|
async def async_get_events(
|
|
|
|
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
2022-04-10 19:04:07 +00:00
|
|
|
) -> list[CalendarEvent]:
|
2018-06-15 15:16:31 +00:00
|
|
|
"""Get all events in a specific time frame."""
|
2022-04-21 03:18:24 +00:00
|
|
|
|
|
|
|
request = ListEventsRequest(
|
|
|
|
calendar_id=self._calendar_id,
|
|
|
|
start_time=start_date,
|
|
|
|
end_time=end_date,
|
|
|
|
search=self._search,
|
|
|
|
)
|
2022-04-27 14:22:15 +00:00
|
|
|
result_items = []
|
|
|
|
try:
|
|
|
|
result = await self._calendar_service.async_list_events(request)
|
|
|
|
async for result_page in result:
|
|
|
|
result_items.extend(result_page.items)
|
|
|
|
except ApiException as err:
|
|
|
|
_LOGGER.error("Unable to connect to Google: %s", err)
|
|
|
|
return []
|
|
|
|
return [
|
|
|
|
_get_calendar_event(event)
|
|
|
|
for event in filter(self._event_filter, result_items)
|
|
|
|
]
|
2021-08-17 03:20:16 +00:00
|
|
|
|
2018-06-15 15:16:31 +00:00
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
2022-03-15 06:51:02 +00:00
|
|
|
async def async_update(self) -> None:
|
2018-06-15 15:16:31 +00:00
|
|
|
"""Get the latest data."""
|
2022-04-21 03:18:24 +00:00
|
|
|
request = ListEventsRequest(calendar_id=self._calendar_id, search=self._search)
|
2022-02-27 00:19:45 +00:00
|
|
|
try:
|
2022-04-21 03:18:24 +00:00
|
|
|
result = await self._calendar_service.async_list_events(request)
|
|
|
|
except ApiException as err:
|
2022-02-27 00:19:45 +00:00
|
|
|
_LOGGER.error("Unable to connect to Google: %s", err)
|
2019-07-11 03:59:37 +00:00
|
|
|
return
|
2017-05-19 14:39:13 +00:00
|
|
|
|
2022-03-27 17:02:19 +00:00
|
|
|
# Pick the first visible event and apply offset calculations.
|
2022-04-21 03:18:24 +00:00
|
|
|
valid_items = filter(self._event_filter, result.items)
|
2022-04-10 19:04:07 +00:00
|
|
|
event = copy.deepcopy(next(valid_items, None))
|
|
|
|
if event:
|
2022-04-21 03:18:24 +00:00
|
|
|
(event.summary, offset) = extract_offset(event.summary, self._offset)
|
2022-04-10 19:04:07 +00:00
|
|
|
self._event = _get_calendar_event(event)
|
2022-04-14 02:04:59 +00:00
|
|
|
self._offset_value = offset
|
2022-04-10 19:04:07 +00:00
|
|
|
else:
|
|
|
|
self._event = None
|
|
|
|
|
|
|
|
|
2022-04-21 03:18:24 +00:00
|
|
|
def _get_calendar_event(event: Event) -> CalendarEvent:
|
2022-04-10 19:04:07 +00:00
|
|
|
"""Return a CalendarEvent from an API event."""
|
|
|
|
return CalendarEvent(
|
2022-04-21 03:18:24 +00:00
|
|
|
summary=event.summary,
|
|
|
|
start=event.start.value,
|
|
|
|
end=event.end.value,
|
|
|
|
description=event.description,
|
|
|
|
location=event.location,
|
2022-04-10 19:04:07 +00:00
|
|
|
)
|