mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
Move google calendar integration to aiohttp (#70173)
* Use new aiohttp based google client library in gcal_sync. * Use base url in tests for shorter string * Remove unnecessary line of code * Jump to gcal-sync-0.4.1 * Update tests/components/google/conftest.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update to gcal_sync 0.5.0 incorporating PR feedback Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
b8369f79eb
commit
0e0c0ce22b
@ -8,7 +8,9 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from httplib2.error import ServerNotFoundError
|
from gcal_sync.api import GoogleCalendarService
|
||||||
|
from gcal_sync.exceptions import ApiException
|
||||||
|
from gcal_sync.model import DateOrDatetime, Event
|
||||||
from oauth2client.file import Storage
|
from oauth2client.file import Storage
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.error import Error as VoluptuousError
|
from voluptuous.error import Error as VoluptuousError
|
||||||
@ -31,13 +33,14 @@ from homeassistant.exceptions import (
|
|||||||
HomeAssistantError,
|
HomeAssistantError,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import config_entry_oauth2_flow
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.entity import generate_entity_id
|
from homeassistant.helpers.entity import generate_entity_id
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from . import config_flow
|
from . import config_flow
|
||||||
from .api import DeviceAuth, GoogleCalendarService
|
from .api import ApiAuthImpl, DeviceAuth
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_CALENDAR_ACCESS,
|
CONF_CALENDAR_ACCESS,
|
||||||
DATA_CONFIG,
|
DATA_CONFIG,
|
||||||
@ -212,7 +215,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(
|
||||||
"Required scopes are not available, reauth required"
|
"Required scopes are not available, reauth required"
|
||||||
)
|
)
|
||||||
calendar_service = GoogleCalendarService(hass, session)
|
calendar_service = GoogleCalendarService(
|
||||||
|
ApiAuthImpl(async_get_clientsession(hass), session)
|
||||||
|
)
|
||||||
hass.data[DOMAIN][DATA_SERVICE] = calendar_service
|
hass.data[DOMAIN][DATA_SERVICE] = calendar_service
|
||||||
|
|
||||||
await async_setup_services(hass, hass.data[DOMAIN][DATA_CONFIG], calendar_service)
|
await async_setup_services(hass, hass.data[DOMAIN][DATA_CONFIG], calendar_service)
|
||||||
@ -263,11 +268,12 @@ async def async_setup_services(
|
|||||||
async def _scan_for_calendars(call: ServiceCall) -> None:
|
async def _scan_for_calendars(call: ServiceCall) -> None:
|
||||||
"""Scan for new calendars."""
|
"""Scan for new calendars."""
|
||||||
try:
|
try:
|
||||||
calendars = await calendar_service.async_list_calendars()
|
result = await calendar_service.async_list_calendars()
|
||||||
except ServerNotFoundError as err:
|
except ApiException as err:
|
||||||
raise HomeAssistantError(str(err)) from err
|
raise HomeAssistantError(str(err)) from err
|
||||||
tasks = []
|
tasks = []
|
||||||
for calendar in calendars:
|
for calendar_item in result.items:
|
||||||
|
calendar = calendar_item.dict(exclude_unset=True)
|
||||||
calendar[CONF_TRACK] = config[CONF_TRACK_NEW]
|
calendar[CONF_TRACK] = config[CONF_TRACK_NEW]
|
||||||
tasks.append(
|
tasks.append(
|
||||||
hass.services.async_call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar)
|
hass.services.async_call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar)
|
||||||
@ -278,8 +284,8 @@ async def async_setup_services(
|
|||||||
|
|
||||||
async def _add_event(call: ServiceCall) -> None:
|
async def _add_event(call: ServiceCall) -> None:
|
||||||
"""Add a new event to calendar."""
|
"""Add a new event to calendar."""
|
||||||
start = {}
|
start: DateOrDatetime | None = None
|
||||||
end = {}
|
end: DateOrDatetime | None = None
|
||||||
|
|
||||||
if EVENT_IN in call.data:
|
if EVENT_IN in call.data:
|
||||||
if EVENT_IN_DAYS in call.data[EVENT_IN]:
|
if EVENT_IN_DAYS in call.data[EVENT_IN]:
|
||||||
@ -288,8 +294,8 @@ async def async_setup_services(
|
|||||||
start_in = now + timedelta(days=call.data[EVENT_IN][EVENT_IN_DAYS])
|
start_in = now + timedelta(days=call.data[EVENT_IN][EVENT_IN_DAYS])
|
||||||
end_in = start_in + timedelta(days=1)
|
end_in = start_in + timedelta(days=1)
|
||||||
|
|
||||||
start = {"date": start_in.strftime("%Y-%m-%d")}
|
start = DateOrDatetime(date=start_in)
|
||||||
end = {"date": end_in.strftime("%Y-%m-%d")}
|
end = DateOrDatetime(date=end_in)
|
||||||
|
|
||||||
elif EVENT_IN_WEEKS in call.data[EVENT_IN]:
|
elif EVENT_IN_WEEKS in call.data[EVENT_IN]:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
@ -297,29 +303,34 @@ async def async_setup_services(
|
|||||||
start_in = now + timedelta(weeks=call.data[EVENT_IN][EVENT_IN_WEEKS])
|
start_in = now + timedelta(weeks=call.data[EVENT_IN][EVENT_IN_WEEKS])
|
||||||
end_in = start_in + timedelta(days=1)
|
end_in = start_in + timedelta(days=1)
|
||||||
|
|
||||||
start = {"date": start_in.strftime("%Y-%m-%d")}
|
start = DateOrDatetime(date=start_in)
|
||||||
end = {"date": end_in.strftime("%Y-%m-%d")}
|
end = DateOrDatetime(date=end_in)
|
||||||
|
|
||||||
elif EVENT_START_DATE in call.data:
|
elif EVENT_START_DATE in call.data:
|
||||||
start = {"date": str(call.data[EVENT_START_DATE])}
|
start = DateOrDatetime(date=call.data[EVENT_START_DATE])
|
||||||
end = {"date": str(call.data[EVENT_END_DATE])}
|
end = DateOrDatetime(date=call.data[EVENT_END_DATE])
|
||||||
|
|
||||||
elif EVENT_START_DATETIME in call.data:
|
elif EVENT_START_DATETIME in call.data:
|
||||||
start_dt = str(
|
start_dt = call.data[EVENT_START_DATETIME]
|
||||||
call.data[EVENT_START_DATETIME].strftime("%Y-%m-%dT%H:%M:%S")
|
end_dt = call.data[EVENT_END_DATETIME]
|
||||||
|
start = DateOrDatetime(
|
||||||
|
date_time=start_dt, timezone=str(hass.config.time_zone)
|
||||||
|
)
|
||||||
|
end = DateOrDatetime(date_time=end_dt, timezone=str(hass.config.time_zone))
|
||||||
|
|
||||||
|
if start is None or end is None:
|
||||||
|
raise ValueError(
|
||||||
|
"Missing required fields to set start or end date/datetime"
|
||||||
)
|
)
|
||||||
end_dt = str(call.data[EVENT_END_DATETIME].strftime("%Y-%m-%dT%H:%M:%S"))
|
|
||||||
start = {"dateTime": start_dt, "timeZone": str(hass.config.time_zone)}
|
|
||||||
end = {"dateTime": end_dt, "timeZone": str(hass.config.time_zone)}
|
|
||||||
|
|
||||||
await calendar_service.async_create_event(
|
await calendar_service.async_create_event(
|
||||||
call.data[EVENT_CALENDAR_ID],
|
call.data[EVENT_CALENDAR_ID],
|
||||||
{
|
Event(
|
||||||
"summary": call.data[EVENT_SUMMARY],
|
summary=call.data[EVENT_SUMMARY],
|
||||||
"description": call.data[EVENT_DESCRIPTION],
|
description=call.data[EVENT_DESCRIPTION],
|
||||||
"start": start,
|
start=start,
|
||||||
"end": end,
|
end=end,
|
||||||
},
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only expose the add event service if we have the correct permissions
|
# Only expose the add event service if we have the correct permissions
|
||||||
|
@ -8,13 +8,13 @@ import logging
|
|||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from googleapiclient import discovery as google_discovery
|
import aiohttp
|
||||||
|
from gcal_sync.auth import AbstractAuth
|
||||||
import oauth2client
|
import oauth2client
|
||||||
from oauth2client.client import (
|
from oauth2client.client import (
|
||||||
Credentials,
|
Credentials,
|
||||||
DeviceFlowInfo,
|
DeviceFlowInfo,
|
||||||
FlowExchangeError,
|
FlowExchangeError,
|
||||||
OAuth2Credentials,
|
|
||||||
OAuth2DeviceCodeError,
|
OAuth2DeviceCodeError,
|
||||||
OAuth2WebServerFlow,
|
OAuth2WebServerFlow,
|
||||||
)
|
)
|
||||||
@ -150,95 +150,19 @@ async def async_create_device_flow(hass: HomeAssistant) -> DeviceFlow:
|
|||||||
return DeviceFlow(hass, oauth_flow, device_flow_info)
|
return DeviceFlow(hass, oauth_flow, device_flow_info)
|
||||||
|
|
||||||
|
|
||||||
def _async_google_creds(hass: HomeAssistant, token: dict[str, Any]) -> Credentials:
|
class ApiAuthImpl(AbstractAuth):
|
||||||
"""Convert a Home Assistant token to a Google API Credentials object."""
|
"""Authentication implementation for google calendar api library."""
|
||||||
conf = hass.data[DOMAIN][DATA_CONFIG]
|
|
||||||
return OAuth2Credentials(
|
|
||||||
access_token=token["access_token"],
|
|
||||||
client_id=conf[CONF_CLIENT_ID],
|
|
||||||
client_secret=conf[CONF_CLIENT_SECRET],
|
|
||||||
refresh_token=token["refresh_token"],
|
|
||||||
token_expiry=datetime.datetime.fromtimestamp(token["expires_at"]),
|
|
||||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
|
||||||
scopes=[conf[CONF_CALENDAR_ACCESS].scope],
|
|
||||||
user_agent=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _api_time_format(date_time: datetime.datetime | None) -> str | None:
|
|
||||||
"""Convert a datetime to the api string format."""
|
|
||||||
return date_time.isoformat("T") if date_time else None
|
|
||||||
|
|
||||||
|
|
||||||
class GoogleCalendarService:
|
|
||||||
"""Calendar service interface to Google."""
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass: HomeAssistant, session: config_entry_oauth2_flow.OAuth2Session
|
self,
|
||||||
|
websession: aiohttp.ClientSession,
|
||||||
|
session: config_entry_oauth2_flow.OAuth2Session,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init the Google Calendar service."""
|
"""Init the Google Calendar client library auth implementation."""
|
||||||
self._hass = hass
|
super().__init__(websession)
|
||||||
self._session = session
|
self._session = session
|
||||||
|
|
||||||
async def _async_get_service(self) -> google_discovery.Resource:
|
async def async_get_access_token(self) -> str:
|
||||||
"""Get the calendar service with valid credetnails."""
|
"""Return a valid access token."""
|
||||||
await self._session.async_ensure_token_valid()
|
await self._session.async_ensure_token_valid()
|
||||||
creds = _async_google_creds(self._hass, self._session.token)
|
return self._session.token["access_token"]
|
||||||
|
|
||||||
def _build() -> google_discovery.Resource:
|
|
||||||
return google_discovery.build(
|
|
||||||
"calendar", "v3", credentials=creds, cache_discovery=False
|
|
||||||
)
|
|
||||||
|
|
||||||
return await self._hass.async_add_executor_job(_build)
|
|
||||||
|
|
||||||
async def async_list_calendars(
|
|
||||||
self,
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Return the list of calendars the user has added to their list."""
|
|
||||||
service = await self._async_get_service()
|
|
||||||
|
|
||||||
def _list_calendars() -> list[dict[str, Any]]:
|
|
||||||
cal_list = service.calendarList()
|
|
||||||
return cal_list.list().execute()["items"]
|
|
||||||
|
|
||||||
return await self._hass.async_add_executor_job(_list_calendars)
|
|
||||||
|
|
||||||
async def async_create_event(
|
|
||||||
self, calendar_id: str, event: dict[str, Any]
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Return the list of calendars the user has added to their list."""
|
|
||||||
service = await self._async_get_service()
|
|
||||||
|
|
||||||
def _create_event() -> dict[str, Any]:
|
|
||||||
events = service.events()
|
|
||||||
return events.insert(calendarId=calendar_id, body=event).execute()
|
|
||||||
|
|
||||||
return await self._hass.async_add_executor_job(_create_event)
|
|
||||||
|
|
||||||
async def async_list_events(
|
|
||||||
self,
|
|
||||||
calendar_id: str,
|
|
||||||
start_time: datetime.datetime | None = None,
|
|
||||||
end_time: datetime.datetime | None = None,
|
|
||||||
search: str | None = None,
|
|
||||||
page_token: str | None = None,
|
|
||||||
) -> tuple[list[dict[str, Any]], str | None]:
|
|
||||||
"""Return the list of events."""
|
|
||||||
service = await self._async_get_service()
|
|
||||||
|
|
||||||
def _list_events() -> tuple[list[dict[str, Any]], str | None]:
|
|
||||||
events = service.events()
|
|
||||||
result = events.list(
|
|
||||||
calendarId=calendar_id,
|
|
||||||
timeMin=_api_time_format(start_time if start_time else dt.now()),
|
|
||||||
timeMax=_api_time_format(end_time),
|
|
||||||
q=search,
|
|
||||||
maxResults=EVENT_PAGE_SIZE,
|
|
||||||
pageToken=page_token,
|
|
||||||
singleEvents=True, # Flattens recurring events
|
|
||||||
orderBy="startTime",
|
|
||||||
).execute()
|
|
||||||
return (result["items"], result.get("nextPageToken"))
|
|
||||||
|
|
||||||
return await self._hass.async_add_executor_job(_list_events)
|
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from httplib2 import ServerNotFoundError
|
from gcal_sync.api import GoogleCalendarService, ListEventsRequest
|
||||||
|
from gcal_sync.exceptions import ApiException
|
||||||
|
from gcal_sync.model import Event
|
||||||
|
|
||||||
from homeassistant.components.calendar import (
|
from homeassistant.components.calendar import (
|
||||||
ENTITY_ID_FORMAT,
|
ENTITY_ID_FORMAT,
|
||||||
@ -22,7 +24,7 @@ from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
|||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import generate_entity_id
|
from homeassistant.helpers.entity import generate_entity_id
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import Throttle, dt
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
CONF_CAL_ID,
|
CONF_CAL_ID,
|
||||||
@ -34,7 +36,6 @@ from . import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SCAN_CALENDARS,
|
SERVICE_SCAN_CALENDARS,
|
||||||
)
|
)
|
||||||
from .api import GoogleCalendarService
|
|
||||||
from .const import DISCOVER_CALENDAR
|
from .const import DISCOVER_CALENDAR
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -147,77 +148,66 @@ class GoogleCalendarEntity(CalendarEntity):
|
|||||||
"""Return the name of the entity."""
|
"""Return the name of the entity."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
def _event_filter(self, event: dict[str, Any]) -> bool:
|
def _event_filter(self, event: Event) -> bool:
|
||||||
"""Return True if the event is visible."""
|
"""Return True if the event is visible."""
|
||||||
if self._ignore_availability:
|
if self._ignore_availability:
|
||||||
return True
|
return True
|
||||||
return event.get(TRANSPARENCY, OPAQUE) == OPAQUE
|
return event.transparency == OPAQUE
|
||||||
|
|
||||||
async def async_get_events(
|
async def async_get_events(
|
||||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||||
) -> list[CalendarEvent]:
|
) -> list[CalendarEvent]:
|
||||||
"""Get all events in a specific time frame."""
|
"""Get all events in a specific time frame."""
|
||||||
event_list: list[dict[str, Any]] = []
|
event_list: list[dict[str, Any]] = []
|
||||||
page_token: str | None = None
|
|
||||||
|
request = ListEventsRequest(
|
||||||
|
calendar_id=self._calendar_id,
|
||||||
|
start_time=start_date,
|
||||||
|
end_time=end_date,
|
||||||
|
search=self._search,
|
||||||
|
)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
items, page_token = await self._calendar_service.async_list_events(
|
result = await self._calendar_service.async_list_events(request)
|
||||||
self._calendar_id,
|
except ApiException as err:
|
||||||
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)
|
_LOGGER.error("Unable to connect to Google: %s", err)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
event_list.extend(filter(self._event_filter, items))
|
event_list.extend(filter(self._event_filter, result.items))
|
||||||
if not page_token:
|
if not result.page_token:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
request.page_token = result.page_token
|
||||||
|
|
||||||
return [_get_calendar_event(event) for event in event_list]
|
return [_get_calendar_event(event) for event in event_list]
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get the latest data."""
|
"""Get the latest data."""
|
||||||
|
request = ListEventsRequest(calendar_id=self._calendar_id, search=self._search)
|
||||||
try:
|
try:
|
||||||
items, _ = await self._calendar_service.async_list_events(
|
result = await self._calendar_service.async_list_events(request)
|
||||||
self._calendar_id, search=self._search
|
except ApiException as err:
|
||||||
)
|
|
||||||
except ServerNotFoundError as err:
|
|
||||||
_LOGGER.error("Unable to connect to Google: %s", err)
|
_LOGGER.error("Unable to connect to Google: %s", err)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Pick the first visible event and apply offset calculations.
|
# Pick the first visible event and apply offset calculations.
|
||||||
valid_items = filter(self._event_filter, items)
|
valid_items = filter(self._event_filter, result.items)
|
||||||
event = copy.deepcopy(next(valid_items, None))
|
event = copy.deepcopy(next(valid_items, None))
|
||||||
if event:
|
if event:
|
||||||
(summary, offset) = extract_offset(event.get("summary", ""), self._offset)
|
(event.summary, offset) = extract_offset(event.summary, self._offset)
|
||||||
event["summary"] = summary
|
|
||||||
self._event = _get_calendar_event(event)
|
self._event = _get_calendar_event(event)
|
||||||
self._offset_value = offset
|
self._offset_value = offset
|
||||||
else:
|
else:
|
||||||
self._event = None
|
self._event = None
|
||||||
|
|
||||||
|
|
||||||
def _get_date_or_datetime(date_dict: dict[str, str]) -> datetime | date:
|
def _get_calendar_event(event: Event) -> CalendarEvent:
|
||||||
"""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 a CalendarEvent from an API event."""
|
||||||
return CalendarEvent(
|
return CalendarEvent(
|
||||||
summary=event["summary"],
|
summary=event.summary,
|
||||||
start=_get_date_or_datetime(event["start"]),
|
start=event.start.value,
|
||||||
end=_get_date_or_datetime(event["end"]),
|
end=event.end.value,
|
||||||
description=event.get("description"),
|
description=event.description,
|
||||||
location=event.get("location"),
|
location=event.location,
|
||||||
)
|
)
|
||||||
|
@ -4,11 +4,7 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["auth"],
|
"dependencies": ["auth"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
|
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
|
||||||
"requirements": [
|
"requirements": ["gcal-sync==0.5.0", "oauth2client==4.1.3"],
|
||||||
"google-api-python-client==2.38.0",
|
|
||||||
"httplib2==0.20.4",
|
|
||||||
"oauth2client==4.1.3"
|
|
||||||
],
|
|
||||||
"codeowners": ["@allenporter"],
|
"codeowners": ["@allenporter"],
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["googleapiclient"]
|
"loggers": ["googleapiclient"]
|
||||||
|
@ -678,6 +678,9 @@ gTTS==2.2.4
|
|||||||
# homeassistant.components.garages_amsterdam
|
# homeassistant.components.garages_amsterdam
|
||||||
garages-amsterdam==3.0.0
|
garages-amsterdam==3.0.0
|
||||||
|
|
||||||
|
# homeassistant.components.google
|
||||||
|
gcal-sync==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.geniushub
|
# homeassistant.components.geniushub
|
||||||
geniushub-client==0.6.30
|
geniushub-client==0.6.30
|
||||||
|
|
||||||
@ -718,9 +721,6 @@ goalzero==0.2.1
|
|||||||
# homeassistant.components.goodwe
|
# homeassistant.components.goodwe
|
||||||
goodwe==0.2.15
|
goodwe==0.2.15
|
||||||
|
|
||||||
# homeassistant.components.google
|
|
||||||
google-api-python-client==2.38.0
|
|
||||||
|
|
||||||
# homeassistant.components.google_pubsub
|
# homeassistant.components.google_pubsub
|
||||||
google-cloud-pubsub==2.11.0
|
google-cloud-pubsub==2.11.0
|
||||||
|
|
||||||
@ -827,7 +827,6 @@ homepluscontrol==0.0.5
|
|||||||
# homeassistant.components.horizon
|
# homeassistant.components.horizon
|
||||||
horimote==0.4.1
|
horimote==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.google
|
|
||||||
# homeassistant.components.remember_the_milk
|
# homeassistant.components.remember_the_milk
|
||||||
httplib2==0.20.4
|
httplib2==0.20.4
|
||||||
|
|
||||||
|
@ -475,6 +475,9 @@ gTTS==2.2.4
|
|||||||
# homeassistant.components.garages_amsterdam
|
# homeassistant.components.garages_amsterdam
|
||||||
garages-amsterdam==3.0.0
|
garages-amsterdam==3.0.0
|
||||||
|
|
||||||
|
# homeassistant.components.google
|
||||||
|
gcal-sync==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.usgs_earthquakes_feed
|
# homeassistant.components.usgs_earthquakes_feed
|
||||||
geojson_client==0.6
|
geojson_client==0.6
|
||||||
|
|
||||||
@ -509,9 +512,6 @@ goalzero==0.2.1
|
|||||||
# homeassistant.components.goodwe
|
# homeassistant.components.goodwe
|
||||||
goodwe==0.2.15
|
goodwe==0.2.15
|
||||||
|
|
||||||
# homeassistant.components.google
|
|
||||||
google-api-python-client==2.38.0
|
|
||||||
|
|
||||||
# homeassistant.components.google_pubsub
|
# homeassistant.components.google_pubsub
|
||||||
google-cloud-pubsub==2.11.0
|
google-cloud-pubsub==2.11.0
|
||||||
|
|
||||||
@ -585,7 +585,6 @@ homematicip==1.0.2
|
|||||||
# homeassistant.components.home_plus_control
|
# homeassistant.components.home_plus_control
|
||||||
homepluscontrol==0.0.5
|
homepluscontrol==0.0.5
|
||||||
|
|
||||||
# homeassistant.components.google
|
|
||||||
# homeassistant.components.remember_the_milk
|
# homeassistant.components.remember_the_milk
|
||||||
httplib2==0.20.4
|
httplib2==0.20.4
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@ from __future__ import annotations
|
|||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Any, Generator, TypeVar
|
from typing import Any, Generator, TypeVar
|
||||||
from unittest.mock import Mock, mock_open, patch
|
from unittest.mock import mock_open, patch
|
||||||
|
|
||||||
from googleapiclient import discovery as google_discovery
|
from gcal_sync.auth import API_BASE_URL
|
||||||
from oauth2client.client import Credentials, OAuth2Credentials
|
from oauth2client.client import Credentials, OAuth2Credentials
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
import yaml
|
||||||
@ -18,6 +18,7 @@ from homeassistant.setup import async_setup_component
|
|||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
|
||||||
ApiResult = Callable[[dict[str, Any]], None]
|
ApiResult = Callable[[dict[str, Any]], None]
|
||||||
ComponentSetup = Callable[[], Awaitable[bool]]
|
ComponentSetup = Callable[[], Awaitable[bool]]
|
||||||
@ -198,22 +199,21 @@ def mock_token_read(
|
|||||||
storage.put(creds)
|
storage.put(creds)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def calendar_resource() -> YieldFixture[google_discovery.Resource]:
|
|
||||||
"""Fixture to mock out the Google discovery API."""
|
|
||||||
with patch("homeassistant.components.google.api.google_discovery.build") as mock:
|
|
||||||
yield mock
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_events_list(
|
def mock_events_list(
|
||||||
calendar_resource: google_discovery.Resource,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
) -> Callable[[dict[str, Any]], None]:
|
) -> ApiResult:
|
||||||
"""Fixture to construct a fake event list API response."""
|
"""Fixture to construct a fake event list API response."""
|
||||||
|
|
||||||
def _put_result(response: dict[str, Any]) -> None:
|
def _put_result(
|
||||||
calendar_resource.return_value.events.return_value.list.return_value.execute.return_value = (
|
response: dict[str, Any], calendar_id: str = None, exc: Exception = None
|
||||||
response
|
) -> None:
|
||||||
|
if calendar_id is None:
|
||||||
|
calendar_id = CALENDAR_ID
|
||||||
|
aioclient_mock.get(
|
||||||
|
f"{API_BASE_URL}/calendars/{calendar_id}/events",
|
||||||
|
json=response,
|
||||||
|
exc=exc,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -235,13 +235,15 @@ def mock_events_list_items(
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_calendars_list(
|
def mock_calendars_list(
|
||||||
calendar_resource: google_discovery.Resource,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
) -> ApiResult:
|
) -> ApiResult:
|
||||||
"""Fixture to construct a fake calendar list API response."""
|
"""Fixture to construct a fake calendar list API response."""
|
||||||
|
|
||||||
def _put_result(response: dict[str, Any]) -> None:
|
def _put_result(response: dict[str, Any], exc=None) -> None:
|
||||||
calendar_resource.return_value.calendarList.return_value.list.return_value.execute.return_value = (
|
aioclient_mock.get(
|
||||||
response
|
f"{API_BASE_URL}/users/me/calendarList",
|
||||||
|
json=response,
|
||||||
|
exc=exc,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -250,12 +252,17 @@ def mock_calendars_list(
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_insert_event(
|
def mock_insert_event(
|
||||||
calendar_resource: google_discovery.Resource,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
) -> Mock:
|
) -> Callable[[..., dict[str, Any]], None]:
|
||||||
"""Fixture to create a mock to capture new events added to the API."""
|
"""Fixture for capturing event creation."""
|
||||||
insert_mock = Mock()
|
|
||||||
calendar_resource.return_value.events.return_value.insert = insert_mock
|
def _expect_result(calendar_id: str = CALENDAR_ID) -> None:
|
||||||
return insert_mock
|
aioclient_mock.post(
|
||||||
|
f"{API_BASE_URL}/calendars/{calendar_id}/events",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
return _expect_result
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
|
@ -8,7 +8,7 @@ from typing import Any
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
import httplib2
|
from aiohttp.client_exceptions import ClientError
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
@ -302,16 +302,19 @@ async def test_missing_summary(hass, mock_events_list_items, component_setup):
|
|||||||
|
|
||||||
|
|
||||||
async def test_update_error(
|
async def test_update_error(
|
||||||
hass, calendar_resource, component_setup, test_api_calendar
|
hass,
|
||||||
|
component_setup,
|
||||||
|
mock_calendars_list,
|
||||||
|
mock_events_list,
|
||||||
|
test_api_calendar,
|
||||||
|
aioclient_mock,
|
||||||
):
|
):
|
||||||
"""Test that the calendar update handles a server error."""
|
"""Test that the calendar update handles a server error."""
|
||||||
|
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
with patch("homeassistant.components.google.api.google_discovery.build") as mock:
|
mock_calendars_list({"items": [test_api_calendar]})
|
||||||
mock.return_value.calendarList.return_value.list.return_value.execute.return_value = {
|
mock_events_list(
|
||||||
"items": [test_api_calendar]
|
{
|
||||||
}
|
|
||||||
mock.return_value.events.return_value.list.return_value.execute.return_value = {
|
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
**TEST_EVENT,
|
**TEST_EVENT,
|
||||||
@ -324,7 +327,8 @@ async def test_update_error(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
assert await component_setup()
|
)
|
||||||
|
assert await component_setup()
|
||||||
|
|
||||||
state = hass.states.get(TEST_ENTITY)
|
state = hass.states.get(TEST_ENTITY)
|
||||||
assert state.name == TEST_ENTITY_NAME
|
assert state.name == TEST_ENTITY_NAME
|
||||||
@ -332,10 +336,11 @@ async def test_update_error(
|
|||||||
|
|
||||||
# Advance time to avoid throttling
|
# Advance time to avoid throttling
|
||||||
now += datetime.timedelta(minutes=30)
|
now += datetime.timedelta(minutes=30)
|
||||||
with patch(
|
|
||||||
"homeassistant.components.google.api.google_discovery.build",
|
aioclient_mock.clear_requests()
|
||||||
side_effect=httplib2.ServerNotFoundError("unit test"),
|
mock_events_list({}, exc=ClientError())
|
||||||
), patch("homeassistant.util.utcnow", return_value=now):
|
|
||||||
|
with patch("homeassistant.util.utcnow", return_value=now):
|
||||||
async_fire_time_changed(hass, now)
|
async_fire_time_changed(hass, now)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
@ -346,10 +351,10 @@ async def test_update_error(
|
|||||||
|
|
||||||
# Advance time beyond update/throttle point
|
# Advance time beyond update/throttle point
|
||||||
now += datetime.timedelta(minutes=30)
|
now += datetime.timedelta(minutes=30)
|
||||||
with patch(
|
|
||||||
"homeassistant.components.google.api.google_discovery.build"
|
aioclient_mock.clear_requests()
|
||||||
) as mock, patch("homeassistant.util.utcnow", return_value=now):
|
mock_events_list(
|
||||||
mock.return_value.events.return_value.list.return_value.execute.return_value = {
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
**TEST_EVENT,
|
**TEST_EVENT,
|
||||||
@ -362,6 +367,9 @@ async def test_update_error(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.util.utcnow", return_value=now):
|
||||||
async_fire_time_changed(hass, now)
|
async_fire_time_changed(hass, now)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
@ -371,8 +379,11 @@ async def test_update_error(
|
|||||||
assert state.state == "off"
|
assert state.state == "off"
|
||||||
|
|
||||||
|
|
||||||
async def test_calendars_api(hass, hass_client, component_setup):
|
async def test_calendars_api(
|
||||||
|
hass, hass_client, component_setup, mock_events_list_items
|
||||||
|
):
|
||||||
"""Test the Rest API returns the calendar."""
|
"""Test the Rest API returns the calendar."""
|
||||||
|
mock_events_list_items([])
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
|
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
@ -388,14 +399,21 @@ async def test_calendars_api(hass, hass_client, component_setup):
|
|||||||
|
|
||||||
|
|
||||||
async def test_http_event_api_failure(
|
async def test_http_event_api_failure(
|
||||||
hass, hass_client, calendar_resource, component_setup
|
hass,
|
||||||
|
hass_client,
|
||||||
|
component_setup,
|
||||||
|
mock_calendars_list,
|
||||||
|
mock_events_list,
|
||||||
|
aioclient_mock,
|
||||||
):
|
):
|
||||||
"""Test the Rest API response during a calendar failure."""
|
"""Test the Rest API response during a calendar failure."""
|
||||||
|
mock_events_list({})
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
|
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
|
|
||||||
calendar_resource.side_effect = httplib2.ServerNotFoundError("unit test")
|
aioclient_mock.clear_requests()
|
||||||
|
mock_events_list({}, exc=ClientError())
|
||||||
|
|
||||||
response = await client.get(upcoming_event_url())
|
response = await client.get(upcoming_event_url())
|
||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
@ -493,16 +511,14 @@ async def test_opaque_event(
|
|||||||
|
|
||||||
async def test_scan_calendar_error(
|
async def test_scan_calendar_error(
|
||||||
hass,
|
hass,
|
||||||
calendar_resource,
|
|
||||||
component_setup,
|
component_setup,
|
||||||
test_api_calendar,
|
test_api_calendar,
|
||||||
|
mock_calendars_list,
|
||||||
):
|
):
|
||||||
"""Test that the calendar update handles a server error."""
|
"""Test that the calendar update handles a server error."""
|
||||||
with patch(
|
|
||||||
"homeassistant.components.google.api.google_discovery.build",
|
mock_calendars_list({}, exc=ClientError())
|
||||||
side_effect=httplib2.ServerNotFoundError("unit test"),
|
assert await component_setup()
|
||||||
):
|
|
||||||
assert await component_setup()
|
|
||||||
|
|
||||||
assert not hass.states.get(TEST_ENTITY)
|
assert not hass.states.get(TEST_ENTITY)
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import datetime
|
|||||||
import http
|
import http
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import Mock, call, patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -134,10 +134,12 @@ async def test_calendar_yaml_error(
|
|||||||
component_setup: ComponentSetup,
|
component_setup: ComponentSetup,
|
||||||
mock_calendars_list: ApiResult,
|
mock_calendars_list: ApiResult,
|
||||||
test_api_calendar: dict[str, Any],
|
test_api_calendar: dict[str, Any],
|
||||||
|
mock_events_list: ApiResult,
|
||||||
setup_config_entry: MockConfigEntry,
|
setup_config_entry: MockConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test setup with yaml file not found."""
|
"""Test setup with yaml file not found."""
|
||||||
mock_calendars_list({"items": [test_api_calendar]})
|
mock_calendars_list({"items": [test_api_calendar]})
|
||||||
|
mock_events_list({})
|
||||||
|
|
||||||
with patch("homeassistant.components.google.open", side_effect=FileNotFoundError()):
|
with patch("homeassistant.components.google.open", side_effect=FileNotFoundError()):
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
@ -182,6 +184,7 @@ async def test_track_new(
|
|||||||
component_setup: ComponentSetup,
|
component_setup: ComponentSetup,
|
||||||
mock_calendars_list: ApiResult,
|
mock_calendars_list: ApiResult,
|
||||||
test_api_calendar: dict[str, Any],
|
test_api_calendar: dict[str, Any],
|
||||||
|
mock_events_list: ApiResult,
|
||||||
mock_calendars_yaml: None,
|
mock_calendars_yaml: None,
|
||||||
expected_state: State,
|
expected_state: State,
|
||||||
setup_config_entry: MockConfigEntry,
|
setup_config_entry: MockConfigEntry,
|
||||||
@ -189,6 +192,7 @@ async def test_track_new(
|
|||||||
"""Test behavior of configuration.yaml settings for tracking new calendars not in the config."""
|
"""Test behavior of configuration.yaml settings for tracking new calendars not in the config."""
|
||||||
|
|
||||||
mock_calendars_list({"items": [test_api_calendar]})
|
mock_calendars_list({"items": [test_api_calendar]})
|
||||||
|
mock_events_list({})
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
|
|
||||||
state = hass.states.get(TEST_API_ENTITY)
|
state = hass.states.get(TEST_API_ENTITY)
|
||||||
@ -202,11 +206,13 @@ async def test_found_calendar_from_api(
|
|||||||
mock_calendars_yaml: None,
|
mock_calendars_yaml: None,
|
||||||
mock_calendars_list: ApiResult,
|
mock_calendars_list: ApiResult,
|
||||||
test_api_calendar: dict[str, Any],
|
test_api_calendar: dict[str, Any],
|
||||||
|
mock_events_list: ApiResult,
|
||||||
setup_config_entry: MockConfigEntry,
|
setup_config_entry: MockConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test finding a calendar from the API."""
|
"""Test finding a calendar from the API."""
|
||||||
|
|
||||||
mock_calendars_list({"items": [test_api_calendar]})
|
mock_calendars_list({"items": [test_api_calendar]})
|
||||||
|
mock_events_list({})
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
|
|
||||||
state = hass.states.get(TEST_API_ENTITY)
|
state = hass.states.get(TEST_API_ENTITY)
|
||||||
@ -240,6 +246,7 @@ async def test_calendar_config_track_new(
|
|||||||
component_setup: ComponentSetup,
|
component_setup: ComponentSetup,
|
||||||
mock_calendars_yaml: None,
|
mock_calendars_yaml: None,
|
||||||
mock_calendars_list: ApiResult,
|
mock_calendars_list: ApiResult,
|
||||||
|
mock_events_list: ApiResult,
|
||||||
test_api_calendar: dict[str, Any],
|
test_api_calendar: dict[str, Any],
|
||||||
calendars_config_track: bool,
|
calendars_config_track: bool,
|
||||||
expected_state: State,
|
expected_state: State,
|
||||||
@ -248,44 +255,35 @@ async def test_calendar_config_track_new(
|
|||||||
"""Test calendar config that overrides whether or not a calendar is tracked."""
|
"""Test calendar config that overrides whether or not a calendar is tracked."""
|
||||||
|
|
||||||
mock_calendars_list({"items": [test_api_calendar]})
|
mock_calendars_list({"items": [test_api_calendar]})
|
||||||
|
mock_events_list({})
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
|
|
||||||
state = hass.states.get(TEST_YAML_ENTITY)
|
state = hass.states.get(TEST_YAML_ENTITY)
|
||||||
assert_state(state, expected_state)
|
assert_state(state, expected_state)
|
||||||
|
|
||||||
|
|
||||||
async def test_add_event(
|
async def test_add_event_missing_required_fields(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
component_setup: ComponentSetup,
|
component_setup: ComponentSetup,
|
||||||
mock_calendars_list: ApiResult,
|
mock_calendars_list: ApiResult,
|
||||||
test_api_calendar: dict[str, Any],
|
test_api_calendar: dict[str, Any],
|
||||||
mock_insert_event: Mock,
|
|
||||||
setup_config_entry: MockConfigEntry,
|
setup_config_entry: MockConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test service call that adds an event."""
|
"""Test service call that adds an event missing required fields."""
|
||||||
|
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
|
|
||||||
await hass.services.async_call(
|
with pytest.raises(ValueError):
|
||||||
DOMAIN,
|
await hass.services.async_call(
|
||||||
SERVICE_ADD_EVENT,
|
DOMAIN,
|
||||||
{
|
SERVICE_ADD_EVENT,
|
||||||
"calendar_id": CALENDAR_ID,
|
{
|
||||||
"summary": "Summary",
|
"calendar_id": CALENDAR_ID,
|
||||||
"description": "Description",
|
"summary": "Summary",
|
||||||
},
|
"description": "Description",
|
||||||
blocking=True,
|
},
|
||||||
)
|
blocking=True,
|
||||||
mock_insert_event.assert_called()
|
)
|
||||||
assert mock_insert_event.mock_calls[0] == call(
|
|
||||||
calendarId=CALENDAR_ID,
|
|
||||||
body={
|
|
||||||
"summary": "Summary",
|
|
||||||
"description": "Description",
|
|
||||||
"start": {},
|
|
||||||
"end": {},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -308,17 +306,27 @@ async def test_add_event_date_in_x(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
component_setup: ComponentSetup,
|
component_setup: ComponentSetup,
|
||||||
mock_calendars_list: ApiResult,
|
mock_calendars_list: ApiResult,
|
||||||
|
mock_insert_event: Callable[[..., dict[str, Any]], None],
|
||||||
test_api_calendar: dict[str, Any],
|
test_api_calendar: dict[str, Any],
|
||||||
mock_insert_event: Mock,
|
|
||||||
date_fields: dict[str, Any],
|
date_fields: dict[str, Any],
|
||||||
start_timedelta: datetime.timedelta,
|
start_timedelta: datetime.timedelta,
|
||||||
end_timedelta: datetime.timedelta,
|
end_timedelta: datetime.timedelta,
|
||||||
setup_config_entry: MockConfigEntry,
|
setup_config_entry: MockConfigEntry,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test service call that adds an event with various time ranges."""
|
"""Test service call that adds an event with various time ranges."""
|
||||||
|
|
||||||
|
mock_calendars_list({})
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
start_date = now + start_timedelta
|
||||||
|
end_date = now + end_timedelta
|
||||||
|
|
||||||
|
mock_insert_event(
|
||||||
|
calendar_id=CALENDAR_ID,
|
||||||
|
)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_ADD_EVENT,
|
SERVICE_ADD_EVENT,
|
||||||
@ -330,38 +338,36 @@ async def test_add_event_date_in_x(
|
|||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
mock_insert_event.assert_called()
|
assert len(aioclient_mock.mock_calls) == 2
|
||||||
|
assert aioclient_mock.mock_calls[1][2] == {
|
||||||
now = datetime.datetime.now()
|
"summary": "Summary",
|
||||||
start_date = now + start_timedelta
|
"description": "Description",
|
||||||
end_date = now + end_timedelta
|
"start": {"date": start_date.date().isoformat()},
|
||||||
|
"end": {"date": end_date.date().isoformat()},
|
||||||
assert mock_insert_event.mock_calls[0] == call(
|
}
|
||||||
calendarId=CALENDAR_ID,
|
|
||||||
body={
|
|
||||||
"summary": "Summary",
|
|
||||||
"description": "Description",
|
|
||||||
"start": {"date": start_date.date().isoformat()},
|
|
||||||
"end": {"date": end_date.date().isoformat()},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_add_event_date(
|
async def test_add_event_date(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
component_setup: ComponentSetup,
|
component_setup: ComponentSetup,
|
||||||
mock_calendars_list: ApiResult,
|
mock_calendars_list: ApiResult,
|
||||||
mock_insert_event: Mock,
|
mock_insert_event: Callable[[str, dict[str, Any]], None],
|
||||||
setup_config_entry: MockConfigEntry,
|
setup_config_entry: MockConfigEntry,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test service call that sets a date range."""
|
"""Test service call that sets a date range."""
|
||||||
|
|
||||||
|
mock_calendars_list({})
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
|
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
today = now.date()
|
today = now.date()
|
||||||
end_date = today + datetime.timedelta(days=2)
|
end_date = today + datetime.timedelta(days=2)
|
||||||
|
|
||||||
|
mock_insert_event(
|
||||||
|
calendar_id=CALENDAR_ID,
|
||||||
|
)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_ADD_EVENT,
|
SERVICE_ADD_EVENT,
|
||||||
@ -374,35 +380,37 @@ async def test_add_event_date(
|
|||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
mock_insert_event.assert_called()
|
assert len(aioclient_mock.mock_calls) == 2
|
||||||
|
assert aioclient_mock.mock_calls[1][2] == {
|
||||||
assert mock_insert_event.mock_calls[0] == call(
|
"summary": "Summary",
|
||||||
calendarId=CALENDAR_ID,
|
"description": "Description",
|
||||||
body={
|
"start": {"date": today.isoformat()},
|
||||||
"summary": "Summary",
|
"end": {"date": end_date.isoformat()},
|
||||||
"description": "Description",
|
}
|
||||||
"start": {"date": today.isoformat()},
|
|
||||||
"end": {"date": end_date.isoformat()},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_add_event_date_time(
|
async def test_add_event_date_time(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
component_setup: ComponentSetup,
|
component_setup: ComponentSetup,
|
||||||
mock_calendars_list: ApiResult,
|
mock_calendars_list: ApiResult,
|
||||||
|
mock_insert_event: Callable[[str, dict[str, Any]], None],
|
||||||
test_api_calendar: dict[str, Any],
|
test_api_calendar: dict[str, Any],
|
||||||
mock_insert_event: Mock,
|
|
||||||
setup_config_entry: MockConfigEntry,
|
setup_config_entry: MockConfigEntry,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test service call that adds an event with a date time range."""
|
"""Test service call that adds an event with a date time range."""
|
||||||
|
|
||||||
|
mock_calendars_list({})
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
|
|
||||||
start_datetime = datetime.datetime.now()
|
start_datetime = datetime.datetime.now()
|
||||||
delta = datetime.timedelta(days=3, hours=3)
|
delta = datetime.timedelta(days=3, hours=3)
|
||||||
end_datetime = start_datetime + delta
|
end_datetime = start_datetime + delta
|
||||||
|
|
||||||
|
mock_insert_event(
|
||||||
|
calendar_id=CALENDAR_ID,
|
||||||
|
)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_ADD_EVENT,
|
SERVICE_ADD_EVENT,
|
||||||
@ -415,34 +423,32 @@ async def test_add_event_date_time(
|
|||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
mock_insert_event.assert_called()
|
assert len(aioclient_mock.mock_calls) == 2
|
||||||
|
assert aioclient_mock.mock_calls[1][2] == {
|
||||||
assert mock_insert_event.mock_calls[0] == call(
|
"summary": "Summary",
|
||||||
calendarId=CALENDAR_ID,
|
"description": "Description",
|
||||||
body={
|
"start": {
|
||||||
"summary": "Summary",
|
"dateTime": start_datetime.isoformat(timespec="seconds"),
|
||||||
"description": "Description",
|
"timeZone": "America/Regina",
|
||||||
"start": {
|
|
||||||
"dateTime": start_datetime.isoformat(timespec="seconds"),
|
|
||||||
"timeZone": "America/Regina",
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"dateTime": end_datetime.isoformat(timespec="seconds"),
|
|
||||||
"timeZone": "America/Regina",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
"end": {
|
||||||
|
"dateTime": end_datetime.isoformat(timespec="seconds"),
|
||||||
|
"timeZone": "America/Regina",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_scan_calendars(
|
async def test_scan_calendars(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
component_setup: ComponentSetup,
|
component_setup: ComponentSetup,
|
||||||
mock_calendars_list: ApiResult,
|
mock_calendars_list: ApiResult,
|
||||||
test_api_calendar: dict[str, Any],
|
mock_events_list: ApiResult,
|
||||||
setup_config_entry: MockConfigEntry,
|
setup_config_entry: MockConfigEntry,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test finding a calendar from the API."""
|
"""Test finding a calendar from the API."""
|
||||||
|
|
||||||
|
mock_calendars_list({"items": []})
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
|
|
||||||
calendar_1 = {
|
calendar_1 = {
|
||||||
@ -454,7 +460,9 @@ async def test_scan_calendars(
|
|||||||
"summary": "Calendar 2",
|
"summary": "Calendar 2",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
mock_calendars_list({"items": [calendar_1]})
|
mock_calendars_list({"items": [calendar_1]})
|
||||||
|
mock_events_list({}, calendar_id="calendar-id-1")
|
||||||
await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True)
|
await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
@ -464,7 +472,10 @@ async def test_scan_calendars(
|
|||||||
assert state.state == STATE_OFF
|
assert state.state == STATE_OFF
|
||||||
assert not hass.states.get("calendar.calendar_2")
|
assert not hass.states.get("calendar.calendar_2")
|
||||||
|
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
mock_calendars_list({"items": [calendar_1, calendar_2]})
|
mock_calendars_list({"items": [calendar_1, calendar_2]})
|
||||||
|
mock_events_list({}, calendar_id="calendar-id-1")
|
||||||
|
mock_events_list({}, calendar_id="calendar-id-2")
|
||||||
await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True)
|
await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user