core/homeassistant/components/google/calendar.py

228 lines
7.2 KiB
Python
Raw Normal View History

"""Support for Google Calendar Search binary sensors."""
from __future__ import annotations
import copy
from datetime import datetime, timedelta
import logging
from typing import Any
2017-05-19 14:39:13 +00:00
from googleapiclient import discovery as google_discovery
from httplib2 import ServerNotFoundError
from homeassistant.components.calendar import (
2019-07-31 19:25:30 +00:00
ENTITY_ID_FORMAT,
CalendarEventDevice,
calculate_offset,
is_offset_reached,
)
2021-02-08 11:24:48 +00:00
from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
2017-05-19 14:39:13 +00:00
from homeassistant.util import Throttle, dt
from . import (
2019-07-31 19:25:30 +00:00
CONF_CAL_ID,
CONF_IGNORE_AVAILABILITY,
CONF_SEARCH,
CONF_TRACK,
DEFAULT_CONF_OFFSET,
TOKEN_FILE,
GoogleCalendarService,
)
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)
# 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
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
disc_info: DiscoveryInfoType | None = None,
) -> None:
2017-05-19 14:39:13 +00:00
"""Set up the calendar platform for event devices."""
if disc_info is None:
return
if not any(data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]):
2017-05-19 14:39:13 +00:00
return
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
entities = []
for data in disc_info[CONF_ENTITIES]:
if not data[CONF_TRACK]:
continue
entity_id = generate_entity_id(
2019-07-31 19:25:30 +00:00
ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass
)
entity = GoogleCalendarEventDevice(
2019-07-31 19:25:30 +00:00
calendar_service, disc_info[CONF_CAL_ID], data, entity_id
)
entities.append(entity)
add_entities(entities, True)
2017-05-19 14:39:13 +00:00
class GoogleCalendarEventDevice(CalendarEventDevice):
"""A calendar event device."""
def __init__(
self,
calendar_service: GoogleCalendarService,
calendar_id: str,
data: dict[str, Any],
entity_id: str,
) -> None:
2017-05-19 14:39:13 +00:00
"""Create the Calendar event device."""
self.data = GoogleCalendarData(
2019-07-31 19:25:30 +00:00
calendar_service,
calendar_id,
2019-07-31 19:25:30 +00:00
data.get(CONF_SEARCH),
data.get(CONF_IGNORE_AVAILABILITY, False),
2019-07-31 19:25:30 +00:00
)
self._event: dict[str, Any] | None = None
self._name: str = data[CONF_NAME]
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
self._offset_reached = False
self.entity_id = entity_id
@property
def extra_state_attributes(self) -> dict[str, bool]:
"""Return the device state attributes."""
2019-07-31 19:25:30 +00:00
return {"offset_reached": self._offset_reached}
@property
def event(self) -> dict[str, Any] | None:
"""Return the next upcoming event."""
return self._event
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name
2017-05-19 14:39:13 +00:00
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[dict[str, Any]]:
"""Get all events in a specific time frame."""
return await self.data.async_get_events(hass, start_date, end_date)
def update(self) -> None:
"""Update event data."""
self.data.update()
event = copy.deepcopy(self.data.event)
if event is None:
self._event = event
return
event = calculate_offset(event, self._offset)
self._offset_reached = is_offset_reached(event)
self._event = event
2017-05-19 14:39:13 +00:00
class GoogleCalendarData:
2017-05-19 14:39:13 +00:00
"""Class to utilize calendar service object to get next event."""
def __init__(
self,
calendar_service: GoogleCalendarService,
calendar_id: str,
search: str | None,
ignore_availability: bool,
) -> None:
2017-05-19 14:39:13 +00:00
"""Set up how we are going to search the google calendar."""
self.calendar_service = calendar_service
self.calendar_id = calendar_id
self.search = search
self.ignore_availability = ignore_availability
self.event: dict[str, Any] | None = None
2017-05-19 14:39:13 +00:00
def _prepare_query(
self,
) -> tuple[google_discovery.Resource | None, dict[str, Any] | None]:
try:
service = self.calendar_service.get()
except ServerNotFoundError as err:
_LOGGER.error("Unable to connect to Google: %s", err)
return None, None
2017-05-19 14:39:13 +00:00
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
2019-07-31 19:25:30 +00:00
params["calendarId"] = self.calendar_id
params["maxResults"] = 100 # Page size
2017-05-19 14:39:13 +00:00
if self.search:
2019-07-31 19:25:30 +00:00
params["q"] = self.search
2017-05-19 14:39:13 +00:00
return service, params
def _event_filter(self, event: dict[str, Any]) -> bool:
"""Return True if the event is visible."""
if self.ignore_availability:
return True
return event.get(TRANSPARENCY, OPAQUE) == OPAQUE
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[dict[str, Any]]:
"""Get all events in a specific time frame."""
2019-07-31 19:25:30 +00:00
service, params = await hass.async_add_executor_job(self._prepare_query)
if service is None or params is None:
return []
2019-07-31 19:25:30 +00:00
params["timeMin"] = start_date.isoformat("T")
params["timeMax"] = end_date.isoformat("T")
event_list: list[dict[str, Any]] = []
events = await hass.async_add_executor_job(service.events)
page_token: str | None = None
while True:
page_token = await self.async_get_events_page(
hass, events, params, page_token, event_list
)
if not page_token:
break
return event_list
async def async_get_events_page(
self,
hass: HomeAssistant,
events: google_discovery.Resource,
params: dict[str, Any],
page_token: str | None,
event_list: list[dict[str, Any]],
) -> str | None:
"""Get a page of events in a specific time frame."""
params["pageToken"] = page_token
2019-07-31 19:25:30 +00:00
result = await hass.async_add_executor_job(events.list(**params).execute)
2019-07-31 19:25:30 +00:00
items = result.get("items", [])
visible_items = filter(self._event_filter, items)
event_list.extend(visible_items)
return result.get("nextPageToken")
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self) -> None:
"""Get the latest data."""
service, params = self._prepare_query()
if service is None or params is None:
return
2019-07-31 19:25:30 +00:00
params["timeMin"] = dt.now().isoformat("T")
events = service.events()
2017-05-19 14:39:13 +00:00
result = events.list(**params).execute()
2019-07-31 19:25:30 +00:00
items = result.get("items", [])
valid_events = filter(self._event_filter, items)
self.event = next(valid_events, None)