2019-02-14 15:01:46 +00:00
|
|
|
"""Support for Google Calendar Search binary sensors."""
|
2022-01-03 12:14:02 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2019-07-11 03:59:37 +00:00
|
|
|
import copy
|
2022-02-01 16:28:32 +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-02-01 16:28:32 +00:00
|
|
|
from googleapiclient import discovery as google_discovery
|
2021-03-02 08:02:04 +00:00
|
|
|
from httplib2 import ServerNotFoundError
|
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,
|
|
|
|
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
|
2022-01-03 12:14:02 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
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
|
|
|
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
2017-05-19 14:39:13 +00:00
|
|
|
from homeassistant.util import Throttle, dt
|
|
|
|
|
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,
|
|
|
|
DEFAULT_CONF_OFFSET,
|
|
|
|
TOKEN_FILE,
|
|
|
|
GoogleCalendarService,
|
|
|
|
)
|
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-01-03 12:14:02 +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
|
|
|
|
|
2017-06-16 19:44:14 +00:00
|
|
|
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))
|
2019-07-11 03:59:37 +00:00
|
|
|
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
|
|
|
|
)
|
2019-07-11 03:59:37 +00:00
|
|
|
entity = GoogleCalendarEventDevice(
|
2019-07-31 19:25:30 +00:00
|
|
|
calendar_service, disc_info[CONF_CAL_ID], data, entity_id
|
|
|
|
)
|
2019-07-11 03:59:37 +00:00
|
|
|
entities.append(entity)
|
|
|
|
|
|
|
|
add_entities(entities, True)
|
2017-05-19 14:39:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
class GoogleCalendarEventDevice(CalendarEventDevice):
|
|
|
|
"""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,
|
|
|
|
) -> None:
|
2017-05-19 14:39:13 +00:00
|
|
|
"""Create the Calendar event device."""
|
2019-07-11 03:59:37 +00:00
|
|
|
self.data = GoogleCalendarData(
|
2019-07-31 19:25:30 +00:00
|
|
|
calendar_service,
|
2022-02-01 16:28:32 +00:00
|
|
|
calendar_id,
|
2019-07-31 19:25:30 +00:00
|
|
|
data.get(CONF_SEARCH),
|
2022-02-01 16:28:32 +00:00
|
|
|
data.get(CONF_IGNORE_AVAILABILITY, False),
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2022-02-01 16:28:32 +00:00
|
|
|
self._event: dict[str, Any] | None = None
|
|
|
|
self._name: str = data[CONF_NAME]
|
2019-07-11 03:59:37 +00:00
|
|
|
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
|
|
|
self._offset_reached = False
|
|
|
|
self.entity_id = entity_id
|
|
|
|
|
|
|
|
@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."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return {"offset_reached": self._offset_reached}
|
2019-07-11 03:59:37 +00:00
|
|
|
|
|
|
|
@property
|
2022-02-01 16:28:32 +00:00
|
|
|
def event(self) -> dict[str, Any] | 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-02-01 16:28:32 +00:00
|
|
|
async def async_get_events(
|
|
|
|
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
|
|
|
) -> list[dict[str, Any]]:
|
2018-06-15 15:16:31 +00:00
|
|
|
"""Get all events in a specific time frame."""
|
|
|
|
return await self.data.async_get_events(hass, start_date, end_date)
|
|
|
|
|
2022-02-01 16:28:32 +00:00
|
|
|
def update(self) -> None:
|
2019-07-11 03:59:37 +00:00
|
|
|
"""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
|
|
|
|
2018-07-20 08:45:20 +00:00
|
|
|
class GoogleCalendarData:
|
2017-05-19 14:39:13 +00:00
|
|
|
"""Class to utilize calendar service object to get next event."""
|
|
|
|
|
2022-02-01 16:28:32 +00:00
|
|
|
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
|
2018-04-06 19:48:50 +00:00
|
|
|
self.ignore_availability = ignore_availability
|
2022-02-01 16:28:32 +00:00
|
|
|
self.event: dict[str, Any] | None = None
|
2017-05-19 14:39:13 +00:00
|
|
|
|
2022-02-01 16:28:32 +00:00
|
|
|
def _prepare_query(
|
|
|
|
self,
|
|
|
|
) -> tuple[google_discovery.Resource | None, dict[str, Any] | None]:
|
2018-03-27 01:10:22 +00:00
|
|
|
try:
|
|
|
|
service = self.calendar_service.get()
|
Increase test coverage for google calendar (#62648)
* Increase test coverage for google calendar
Update tests to exercise the API responses, getting test coverage
to 97% for calendar.py
----------- coverage: platform linux, python 3.9.6-final-0 -----------
Name Stmts Miss Cover Missing
---------------------------------------------------------------------------
homeassistant/components/google/__init__.py 193 84 56% 92, 163-228, 238, 244-247, 254-262, 274, 298-299, 305-347, 387-392, 416-430, 435-437
homeassistant/components/google/calendar.py 122 4 97% 41, 45, 51, 135
---------------------------------------------------------------------------
TOTAL 315 88 72%
* Revert conftest changes
* Update typing errors found on CI
* Update python3.8 typing imports
* Remove commented out code
2021-12-23 06:31:56 +00:00
|
|
|
except ServerNotFoundError as err:
|
|
|
|
_LOGGER.error("Unable to connect to Google: %s", err)
|
2019-05-05 16:38:55 +00:00
|
|
|
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
|
2021-08-17 03:20:16 +00:00
|
|
|
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
|
|
|
|
2018-06-15 15:16:31 +00:00
|
|
|
return service, params
|
|
|
|
|
2022-02-26 23:27:08 +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
|
|
|
|
|
2022-02-01 16:28:32 +00:00
|
|
|
async def async_get_events(
|
|
|
|
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
|
|
|
) -> list[dict[str, Any]]:
|
2018-06-15 15:16:31 +00:00
|
|
|
"""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)
|
2022-02-01 16:28:32 +00:00
|
|
|
if service is None or params is None:
|
2019-07-11 03:59:37 +00:00
|
|
|
return []
|
2019-07-31 19:25:30 +00:00
|
|
|
params["timeMin"] = start_date.isoformat("T")
|
|
|
|
params["timeMax"] = end_date.isoformat("T")
|
2018-06-15 15:16:31 +00:00
|
|
|
|
2022-02-01 16:28:32 +00:00
|
|
|
event_list: list[dict[str, Any]] = []
|
2019-05-05 16:38:55 +00:00
|
|
|
events = await hass.async_add_executor_job(service.events)
|
2022-02-01 16:28:32 +00:00
|
|
|
page_token: str | None = None
|
2021-08-17 03:20:16 +00:00
|
|
|
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
|
|
|
|
|
2022-02-01 16:28:32 +00:00
|
|
|
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:
|
2021-08-17 03:20:16 +00:00
|
|
|
"""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)
|
2018-06-15 15:16:31 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
items = result.get("items", [])
|
2022-02-26 23:27:08 +00:00
|
|
|
visible_items = filter(self._event_filter, items)
|
|
|
|
event_list.extend(visible_items)
|
2021-08-17 03:20:16 +00:00
|
|
|
return result.get("nextPageToken")
|
2018-06-15 15:16:31 +00:00
|
|
|
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
2022-02-01 16:28:32 +00:00
|
|
|
def update(self) -> None:
|
2018-06-15 15:16:31 +00:00
|
|
|
"""Get the latest data."""
|
|
|
|
service, params = self._prepare_query()
|
2022-02-01 16:28:32 +00:00
|
|
|
if service is None or params is None:
|
2019-07-11 03:59:37 +00:00
|
|
|
return
|
2019-07-31 19:25:30 +00:00
|
|
|
params["timeMin"] = dt.now().isoformat("T")
|
2018-06-15 15:16:31 +00:00
|
|
|
|
2018-06-25 17:05:07 +00:00
|
|
|
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", [])
|
2022-02-26 23:27:08 +00:00
|
|
|
valid_events = filter(self._event_filter, items)
|
|
|
|
self.event = next(valid_events, None)
|