core/homeassistant/components/google/calendar.py

217 lines
7.0 KiB
Python
Raw Normal View History

"""Support for Google Calendar Search binary sensors."""
from __future__ import annotations
import copy
from datetime import date, datetime, timedelta
import logging
from typing import Any
2017-05-19 14:39:13 +00:00
from httplib2 import ServerNotFoundError
from homeassistant.components.calendar import (
2019-07-31 19:25:30 +00:00
ENTITY_ID_FORMAT,
CalendarEntity,
CalendarEvent,
extract_offset,
2019-07-31 19:25:30 +00:00
is_offset_reached,
)
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
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle, dt
2017-05-19 14:39:13 +00:00
from . import (
2019-07-31 19:25:30 +00:00
CONF_CAL_ID,
CONF_IGNORE_AVAILABILITY,
CONF_SEARCH,
CONF_TRACK,
DATA_SERVICE,
2019-07-31 19:25:30 +00:00
DEFAULT_CONF_OFFSET,
DOMAIN,
SERVICE_SCAN_CALENDARS,
2019-07-31 19:25:30 +00:00
)
from .api import GoogleCalendarService
from .const import DISCOVER_CALENDAR
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
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""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
# 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
@callback
def _async_setup_entities(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
disc_info: dict[str, Any],
) -> None:
calendar_service = hass.data[DOMAIN][DATA_SERVICE]
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 = GoogleCalendarEntity(
2019-07-31 19:25:30 +00:00
calendar_service, disc_info[CONF_CAL_ID], data, entity_id
)
entities.append(entity)
async_add_entities(entities, True)
2017-05-19 14:39:13 +00:00
class GoogleCalendarEntity(CalendarEntity):
2017-05-19 14:39:13 +00:00
"""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._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)
self._event: CalendarEvent | 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) -> CalendarEvent | 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
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[CalendarEvent]:
"""Get all events in a specific time frame."""
event_list: list[dict[str, Any]] = []
page_token: str | None = None
while True:
try:
items, page_token = await self._calendar_service.async_list_events(
self._calendar_id,
start_time=start_date,
end_time=end_date,
search=self._search,
page_token=page_token,
)
except ServerNotFoundError as err:
_LOGGER.error("Unable to connect to Google: %s", err)
return []
event_list.extend(filter(self._event_filter, items))
if not page_token:
break
return [_get_calendar_event(event) for event in event_list]
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self) -> None:
"""Get the latest data."""
try:
items, _ = await self._calendar_service.async_list_events(
self._calendar_id, search=self._search
)
except ServerNotFoundError as err:
_LOGGER.error("Unable to connect to Google: %s", err)
return
2017-05-19 14:39:13 +00:00
# Pick the first visible event and apply offset calculations.
valid_items = filter(self._event_filter, items)
event = copy.deepcopy(next(valid_items, None))
if event:
(summary, offset) = extract_offset(event.get("summary", ""), self._offset)
event["summary"] = summary
self._event = _get_calendar_event(event)
self._offset_reached = is_offset_reached(
self._event.start_datetime_local, offset
)
else:
self._event = None
def _get_date_or_datetime(date_dict: dict[str, str]) -> datetime | date:
"""Convert a google calendar API response to a datetime or date object."""
if "date" in date_dict:
parsed_date = dt.parse_date(date_dict["date"])
assert parsed_date
return parsed_date
parsed_datetime = dt.parse_datetime(date_dict["dateTime"])
assert parsed_datetime
return parsed_datetime
def _get_calendar_event(event: dict[str, Any]) -> CalendarEvent:
"""Return a CalendarEvent from an API event."""
return CalendarEvent(
summary=event["summary"],
start=_get_date_or_datetime(event["start"]),
end=_get_date_or_datetime(event["end"]),
description=event.get("description"),
location=event.get("location"),
)