2019-02-14 15:01:46 +00:00
|
|
|
"""Support for Google - Calendar Event Devices."""
|
2022-02-26 21:23:11 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
import asyncio
|
2022-02-01 16:28:32 +00:00
|
|
|
from collections.abc import Mapping
|
2022-03-15 06:51:02 +00:00
|
|
|
from datetime import datetime, timedelta
|
2016-11-19 06:29:20 +00:00
|
|
|
import logging
|
2022-02-01 16:28:32 +00:00
|
|
|
from typing import Any
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
from httplib2.error import ServerNotFoundError
|
2019-10-18 00:11:51 +00:00
|
|
|
from oauth2client.file import Storage
|
2016-11-19 06:29:20 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
from voluptuous.error import Error as VoluptuousError
|
2019-12-09 13:17:36 +00:00
|
|
|
import yaml
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
from homeassistant import config_entries
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2021-02-08 11:24:48 +00:00
|
|
|
from homeassistant.const import (
|
|
|
|
CONF_CLIENT_ID,
|
|
|
|
CONF_CLIENT_SECRET,
|
|
|
|
CONF_DEVICE_ID,
|
|
|
|
CONF_ENTITIES,
|
|
|
|
CONF_NAME,
|
|
|
|
CONF_OFFSET,
|
|
|
|
)
|
2022-03-15 06:51:02 +00:00
|
|
|
from homeassistant.core import HomeAssistant, ServiceCall
|
|
|
|
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
|
|
|
from homeassistant.helpers import config_entry_oauth2_flow
|
2019-12-09 13:17:36 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2022-03-15 06:51:02 +00:00
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
2016-11-19 06:29:20 +00:00
|
|
|
from homeassistant.helpers.entity import generate_entity_id
|
2022-01-01 21:38:11 +00:00
|
|
|
from homeassistant.helpers.typing import ConfigType
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
from . import config_flow
|
|
|
|
from .api import DeviceAuth, GoogleCalendarService
|
|
|
|
from .const import (
|
|
|
|
CONF_CALENDAR_ACCESS,
|
|
|
|
DATA_CONFIG,
|
|
|
|
DATA_SERVICE,
|
|
|
|
DISCOVER_CALENDAR,
|
|
|
|
DOMAIN,
|
|
|
|
FeatureAccess,
|
|
|
|
)
|
2022-02-27 00:19:45 +00:00
|
|
|
|
2016-11-19 06:29:20 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
|
|
|
|
|
|
|
CONF_TRACK_NEW = "track_new_calendar"
|
|
|
|
|
|
|
|
CONF_CAL_ID = "cal_id"
|
|
|
|
CONF_TRACK = "track"
|
|
|
|
CONF_SEARCH = "search"
|
|
|
|
CONF_IGNORE_AVAILABILITY = "ignore_availability"
|
|
|
|
CONF_MAX_RESULTS = "max_results"
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DEFAULT_CONF_OFFSET = "!!"
|
|
|
|
|
|
|
|
EVENT_CALENDAR_ID = "calendar_id"
|
|
|
|
EVENT_DESCRIPTION = "description"
|
|
|
|
EVENT_END_CONF = "end"
|
|
|
|
EVENT_END_DATE = "end_date"
|
|
|
|
EVENT_END_DATETIME = "end_date_time"
|
|
|
|
EVENT_IN = "in"
|
|
|
|
EVENT_IN_DAYS = "days"
|
|
|
|
EVENT_IN_WEEKS = "weeks"
|
|
|
|
EVENT_START_CONF = "start"
|
|
|
|
EVENT_START_DATE = "start_date"
|
|
|
|
EVENT_START_DATETIME = "start_date_time"
|
|
|
|
EVENT_SUMMARY = "summary"
|
|
|
|
EVENT_TYPES_CONF = "event_types"
|
|
|
|
|
|
|
|
NOTIFICATION_ID = "google_calendar_notification"
|
2019-06-02 20:58:27 +00:00
|
|
|
NOTIFICATION_TITLE = "Google Calendar Setup"
|
2016-11-19 06:29:20 +00:00
|
|
|
GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors"
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_SCAN_CALENDARS = "scan_for_calendars"
|
|
|
|
SERVICE_FOUND_CALENDARS = "found_calendar"
|
|
|
|
SERVICE_ADD_EVENT = "add_event"
|
|
|
|
|
2019-09-03 15:10:56 +00:00
|
|
|
YAML_DEVICES = f"{DOMAIN}_calendars.yaml"
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2019-09-03 15:10:56 +00:00
|
|
|
TOKEN_FILE = f".{DOMAIN}.token"
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
PLATFORMS = ["calendar"]
|
2021-07-25 19:33:21 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
DOMAIN: vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CLIENT_ID): cv.string,
|
|
|
|
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
2022-03-07 08:44:32 +00:00
|
|
|
vol.Optional(CONF_TRACK_NEW, default=True): cv.boolean,
|
2021-07-25 19:33:21 +00:00
|
|
|
vol.Optional(CONF_CALENDAR_ACCESS, default="read_write"): cv.enum(
|
|
|
|
FeatureAccess
|
|
|
|
),
|
2019-07-31 19:25:30 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
extra=vol.ALLOW_EXTRA,
|
|
|
|
)
|
|
|
|
|
2021-08-17 03:20:16 +00:00
|
|
|
_SINGLE_CALSEARCH_CONFIG = vol.All(
|
|
|
|
cv.deprecated(CONF_MAX_RESULTS),
|
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_NAME): cv.string,
|
|
|
|
vol.Required(CONF_DEVICE_ID): cv.string,
|
|
|
|
vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean,
|
|
|
|
vol.Optional(CONF_OFFSET): cv.string,
|
|
|
|
vol.Optional(CONF_SEARCH): cv.string,
|
|
|
|
vol.Optional(CONF_TRACK): cv.boolean,
|
|
|
|
vol.Optional(CONF_MAX_RESULTS): cv.positive_int, # Now unused
|
|
|
|
}
|
|
|
|
),
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
DEVICE_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CAL_ID): cv.string,
|
|
|
|
vol.Required(CONF_ENTITIES, None): vol.All(
|
|
|
|
cv.ensure_list, [_SINGLE_CALSEARCH_CONFIG]
|
|
|
|
),
|
|
|
|
},
|
|
|
|
extra=vol.ALLOW_EXTRA,
|
|
|
|
)
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2019-06-02 20:58:27 +00:00
|
|
|
_EVENT_IN_TYPES = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES_CONF): cv.positive_int,
|
|
|
|
vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES_CONF): cv.positive_int,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
ADD_EVENT_SERVICE_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(EVENT_CALENDAR_ID): cv.string,
|
|
|
|
vol.Required(EVENT_SUMMARY): cv.string,
|
|
|
|
vol.Optional(EVENT_DESCRIPTION, default=""): cv.string,
|
|
|
|
vol.Exclusive(EVENT_START_DATE, EVENT_START_CONF): cv.date,
|
|
|
|
vol.Exclusive(EVENT_END_DATE, EVENT_END_CONF): cv.date,
|
|
|
|
vol.Exclusive(EVENT_START_DATETIME, EVENT_START_CONF): cv.datetime,
|
|
|
|
vol.Exclusive(EVENT_END_DATETIME, EVENT_END_CONF): cv.datetime,
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Exclusive(EVENT_IN, EVENT_START_CONF, EVENT_END_CONF): _EVENT_IN_TYPES,
|
2019-06-02 20:58:27 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
|
|
"""Set up the Google component."""
|
|
|
|
conf = config.get(DOMAIN, {})
|
|
|
|
hass.data[DOMAIN] = {DATA_CONFIG: conf}
|
|
|
|
config_flow.OAuth2FlowHandler.async_register_implementation(
|
2022-01-11 17:51:16 +00:00
|
|
|
hass,
|
2022-03-15 06:51:02 +00:00
|
|
|
DeviceAuth(
|
|
|
|
hass,
|
|
|
|
conf[CONF_CLIENT_ID],
|
|
|
|
conf[CONF_CLIENT_SECRET],
|
2019-07-31 19:25:30 +00:00
|
|
|
),
|
2016-11-19 06:29:20 +00:00
|
|
|
)
|
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
# Import credentials from the old token file into the new way as
|
|
|
|
# a ConfigEntry managed by home assistant.
|
|
|
|
storage = Storage(hass.config.path(TOKEN_FILE))
|
|
|
|
creds = await hass.async_add_executor_job(storage.get)
|
|
|
|
if creds and conf[CONF_CALENDAR_ACCESS].scope in creds.scopes:
|
|
|
|
_LOGGER.debug("Importing configuration entry with credentials")
|
|
|
|
hass.async_create_task(
|
|
|
|
hass.config_entries.flow.async_init(
|
|
|
|
DOMAIN,
|
|
|
|
context={"source": config_entries.SOURCE_IMPORT},
|
|
|
|
data={
|
|
|
|
"creds": creds,
|
|
|
|
},
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2022-03-15 06:51:02 +00:00
|
|
|
)
|
|
|
|
return True
|
2016-11-19 06:29:20 +00:00
|
|
|
|
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
|
|
"""Set up Google from a config entry."""
|
|
|
|
implementation = (
|
|
|
|
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
|
|
|
hass, entry
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
|
|
|
)
|
2022-03-15 06:51:02 +00:00
|
|
|
assert isinstance(implementation, DeviceAuth)
|
|
|
|
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
|
|
|
required_scope = hass.data[DOMAIN][DATA_CONFIG][CONF_CALENDAR_ACCESS].scope
|
|
|
|
if required_scope not in session.token.get("scope", []):
|
|
|
|
raise ConfigEntryAuthFailed(
|
|
|
|
"Required scopes are not available, reauth required"
|
|
|
|
)
|
|
|
|
calendar_service = GoogleCalendarService(hass, session)
|
|
|
|
hass.data[DOMAIN][DATA_SERVICE] = calendar_service
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
await async_setup_services(hass, hass.data[DOMAIN][DATA_CONFIG], calendar_service)
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
return True
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2019-06-02 20:58:27 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
|
|
"""Unload a config entry."""
|
|
|
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
2019-06-02 20:58:27 +00:00
|
|
|
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
async def async_setup_services(
|
2022-02-01 16:28:32 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
config: ConfigType,
|
|
|
|
calendar_service: GoogleCalendarService,
|
|
|
|
) -> None:
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Set up the service listeners."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2022-03-08 20:56:49 +00:00
|
|
|
created_calendars = set()
|
2022-03-15 06:51:02 +00:00
|
|
|
calendars = await hass.async_add_executor_job(
|
|
|
|
load_config, hass.config.path(YAML_DEVICES)
|
|
|
|
)
|
2022-03-08 20:56:49 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
async def _found_calendar(call: ServiceCall) -> None:
|
2016-11-19 06:29:20 +00:00
|
|
|
calendar = get_calendar_info(hass, call.data)
|
2022-02-26 20:30:04 +00:00
|
|
|
calendar_id = calendar[CONF_CAL_ID]
|
2022-03-08 20:56:49 +00:00
|
|
|
|
|
|
|
if calendar_id in created_calendars:
|
2016-11-19 06:29:20 +00:00
|
|
|
return
|
2022-03-08 20:56:49 +00:00
|
|
|
created_calendars.add(calendar_id)
|
|
|
|
|
|
|
|
# Populate the yaml file with all discovered calendars
|
|
|
|
if calendar_id not in calendars:
|
|
|
|
calendars[calendar_id] = calendar
|
2022-03-15 06:51:02 +00:00
|
|
|
await hass.async_add_executor_job(
|
|
|
|
update_config, hass.config.path(YAML_DEVICES), calendar
|
|
|
|
)
|
2022-03-08 20:56:49 +00:00
|
|
|
else:
|
|
|
|
# Prefer entity/name information from yaml, overriding api
|
|
|
|
calendar = calendars[calendar_id]
|
2022-03-15 06:51:02 +00:00
|
|
|
async_dispatcher_send(hass, DISCOVER_CALENDAR, calendar)
|
2022-03-08 20:56:49 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
hass.services.async_register(DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar)
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
async def _scan_for_calendars(call: ServiceCall) -> None:
|
2016-11-19 06:29:20 +00:00
|
|
|
"""Scan for new calendars."""
|
2022-03-15 06:51:02 +00:00
|
|
|
try:
|
|
|
|
calendars = await calendar_service.async_list_calendars()
|
|
|
|
except ServerNotFoundError as err:
|
|
|
|
raise HomeAssistantError(str(err)) from err
|
|
|
|
tasks = []
|
2016-11-19 06:29:20 +00:00
|
|
|
for calendar in calendars:
|
2022-03-07 08:44:32 +00:00
|
|
|
calendar[CONF_TRACK] = config[CONF_TRACK_NEW]
|
2022-03-15 06:51:02 +00:00
|
|
|
tasks.append(
|
|
|
|
hass.services.async_call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar)
|
|
|
|
)
|
|
|
|
await asyncio.gather(*tasks)
|
2016-11-19 06:29:20 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
hass.services.async_register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars)
|
2019-06-02 20:58:27 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
async def _add_event(call: ServiceCall) -> None:
|
2019-06-02 20:58:27 +00:00
|
|
|
"""Add a new event to calendar."""
|
|
|
|
start = {}
|
|
|
|
end = {}
|
|
|
|
|
|
|
|
if EVENT_IN in call.data:
|
|
|
|
if EVENT_IN_DAYS in call.data[EVENT_IN]:
|
|
|
|
now = datetime.now()
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
start_in = now + timedelta(days=call.data[EVENT_IN][EVENT_IN_DAYS])
|
2019-06-02 20:58:27 +00:00
|
|
|
end_in = start_in + timedelta(days=1)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
start = {"date": start_in.strftime("%Y-%m-%d")}
|
|
|
|
end = {"date": end_in.strftime("%Y-%m-%d")}
|
2019-06-02 20:58:27 +00:00
|
|
|
|
|
|
|
elif EVENT_IN_WEEKS in call.data[EVENT_IN]:
|
|
|
|
now = datetime.now()
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
start_in = now + timedelta(weeks=call.data[EVENT_IN][EVENT_IN_WEEKS])
|
2019-06-02 20:58:27 +00:00
|
|
|
end_in = start_in + timedelta(days=1)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
start = {"date": start_in.strftime("%Y-%m-%d")}
|
|
|
|
end = {"date": end_in.strftime("%Y-%m-%d")}
|
2019-06-02 20:58:27 +00:00
|
|
|
|
|
|
|
elif EVENT_START_DATE in call.data:
|
2019-07-31 19:25:30 +00:00
|
|
|
start = {"date": str(call.data[EVENT_START_DATE])}
|
|
|
|
end = {"date": str(call.data[EVENT_END_DATE])}
|
2019-06-02 20:58:27 +00:00
|
|
|
|
|
|
|
elif EVENT_START_DATETIME in call.data:
|
2019-07-31 19:25:30 +00:00
|
|
|
start_dt = str(
|
|
|
|
call.data[EVENT_START_DATETIME].strftime("%Y-%m-%dT%H:%M:%S")
|
|
|
|
)
|
|
|
|
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)}
|
2019-06-02 20:58:27 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
await calendar_service.async_create_event(
|
2022-02-27 00:19:45 +00:00
|
|
|
call.data[EVENT_CALENDAR_ID],
|
|
|
|
{
|
|
|
|
"summary": call.data[EVENT_SUMMARY],
|
|
|
|
"description": call.data[EVENT_DESCRIPTION],
|
|
|
|
"start": start,
|
|
|
|
"end": end,
|
|
|
|
},
|
|
|
|
)
|
2019-06-02 20:58:27 +00:00
|
|
|
|
2021-07-25 19:33:21 +00:00
|
|
|
# Only expose the add event service if we have the correct permissions
|
|
|
|
if config.get(CONF_CALENDAR_ACCESS) is FeatureAccess.read_write:
|
2022-03-15 06:51:02 +00:00
|
|
|
hass.services.async_register(
|
2021-07-25 19:33:21 +00:00
|
|
|
DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA
|
|
|
|
)
|
2016-11-19 06:29:20 +00:00
|
|
|
|
|
|
|
|
2022-02-01 16:28:32 +00:00
|
|
|
def get_calendar_info(
|
|
|
|
hass: HomeAssistant, calendar: Mapping[str, Any]
|
|
|
|
) -> dict[str, Any]:
|
2016-11-19 06:29:20 +00:00
|
|
|
"""Convert data from Google into DEVICE_SCHEMA."""
|
2022-02-01 16:28:32 +00:00
|
|
|
calendar_info: dict[str, Any] = DEVICE_SCHEMA(
|
2019-07-31 19:25:30 +00:00
|
|
|
{
|
|
|
|
CONF_CAL_ID: calendar["id"],
|
|
|
|
CONF_ENTITIES: [
|
|
|
|
{
|
|
|
|
CONF_TRACK: calendar["track"],
|
|
|
|
CONF_NAME: calendar["summary"],
|
|
|
|
CONF_DEVICE_ID: generate_entity_id(
|
|
|
|
"{}", calendar["summary"], hass=hass
|
|
|
|
),
|
|
|
|
}
|
|
|
|
],
|
|
|
|
}
|
|
|
|
)
|
2016-11-19 06:29:20 +00:00
|
|
|
return calendar_info
|
|
|
|
|
|
|
|
|
2022-02-01 16:28:32 +00:00
|
|
|
def load_config(path: str) -> dict[str, Any]:
|
2016-11-19 06:29:20 +00:00
|
|
|
"""Load the google_calendar_devices.yaml."""
|
|
|
|
calendars = {}
|
|
|
|
try:
|
2021-07-28 07:41:45 +00:00
|
|
|
with open(path, encoding="utf8") as file:
|
2017-10-13 05:05:33 +00:00
|
|
|
data = yaml.safe_load(file)
|
2016-11-19 06:29:20 +00:00
|
|
|
for calendar in data:
|
|
|
|
try:
|
2019-07-31 19:25:30 +00:00
|
|
|
calendars.update({calendar[CONF_CAL_ID]: DEVICE_SCHEMA(calendar)})
|
2016-11-19 06:29:20 +00:00
|
|
|
except VoluptuousError as exception:
|
|
|
|
# keep going
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.warning("Calendar Invalid Data: %s", exception)
|
2022-03-08 20:56:49 +00:00
|
|
|
except FileNotFoundError as err:
|
|
|
|
_LOGGER.debug("Error reading calendar configuration: %s", err)
|
2016-11-19 06:29:20 +00:00
|
|
|
# When YAML file could not be loaded/did not contain a dict
|
|
|
|
return {}
|
|
|
|
|
|
|
|
return calendars
|
|
|
|
|
|
|
|
|
2022-02-01 16:28:32 +00:00
|
|
|
def update_config(path: str, calendar: dict[str, Any]) -> None:
|
2016-11-19 06:29:20 +00:00
|
|
|
"""Write the google_calendar_devices.yaml."""
|
2022-03-08 20:56:49 +00:00
|
|
|
try:
|
|
|
|
with open(path, "a", encoding="utf8") as out:
|
|
|
|
out.write("\n")
|
|
|
|
yaml.dump([calendar], out, default_flow_style=False)
|
|
|
|
except FileNotFoundError as err:
|
|
|
|
_LOGGER.debug("Error persisting calendar configuration: %s", err)
|