1 """Support for Google - Calendar Event Devices."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
6 from datetime
import datetime, timedelta
11 from gcal_sync.api
import GoogleCalendarService
12 from gcal_sync.exceptions
import ApiException, AuthException
13 from gcal_sync.model
import DateOrDatetime, Event
14 import voluptuous
as vol
27 ConfigEntryAuthFailed,
36 from .api
import ApiAuthImpl, get_feature_access
54 from .store
import LocalCalendarStore
56 _LOGGER = logging.getLogger(__name__)
58 ENTITY_ID_FORMAT = DOMAIN +
".{}"
60 CONF_TRACK_NEW =
"track_new_calendar"
62 CONF_CAL_ID =
"cal_id"
64 CONF_SEARCH =
"search"
65 CONF_IGNORE_AVAILABILITY =
"ignore_availability"
66 CONF_MAX_RESULTS =
"max_results"
68 DEFAULT_CONF_OFFSET =
"!!"
70 EVENT_CALENDAR_ID =
"calendar_id"
72 SERVICE_ADD_EVENT =
"add_event"
74 YAML_DEVICES = f
"{DOMAIN}_calendars.yaml"
76 PLATFORMS = [Platform.CALENDAR]
79 CONFIG_SCHEMA = vol.Schema(cv.removed(DOMAIN), extra=vol.ALLOW_EXTRA)
82 _SINGLE_CALSEARCH_CONFIG = vol.All(
83 cv.deprecated(CONF_MAX_RESULTS),
86 vol.Required(CONF_NAME): cv.string,
87 vol.Required(CONF_DEVICE_ID): cv.string,
88 vol.Optional(CONF_IGNORE_AVAILABILITY, default=
True): cv.boolean,
89 vol.Optional(CONF_OFFSET): cv.string,
90 vol.Optional(CONF_SEARCH): cv.string,
91 vol.Optional(CONF_TRACK): cv.boolean,
92 vol.Optional(CONF_MAX_RESULTS): cv.positive_int,
97 DEVICE_SCHEMA = vol.Schema(
99 vol.Required(CONF_CAL_ID): cv.string,
100 vol.Required(CONF_ENTITIES,
None): vol.All(
101 cv.ensure_list, [_SINGLE_CALSEARCH_CONFIG]
104 extra=vol.ALLOW_EXTRA,
107 _EVENT_IN_TYPES = vol.Schema(
109 vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES_CONF): cv.positive_int,
110 vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES_CONF): cv.positive_int,
114 ADD_EVENT_SERVICE_SCHEMA = vol.All(
115 cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
116 cv.has_at_most_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
118 vol.Required(EVENT_CALENDAR_ID): cv.string,
119 vol.Required(EVENT_SUMMARY): cv.string,
120 vol.Optional(EVENT_DESCRIPTION, default=
""): cv.string,
121 vol.Optional(EVENT_LOCATION, default=
""): cv.string,
123 EVENT_START_DATE,
"dates",
"Start and end dates must both be specified"
126 EVENT_END_DATE,
"dates",
"Start and end dates must both be specified"
129 EVENT_START_DATETIME,
131 "Start and end datetimes must both be specified",
136 "Start and end datetimes must both be specified",
138 vol.Optional(EVENT_IN): _EVENT_IN_TYPES,
144 """Set up Google from a config entry."""
145 hass.data.setdefault(DOMAIN, {})
146 hass.data[DOMAIN][entry.entry_id] = {}
151 await hass.async_add_executor_job(load_config, hass.config.path(YAML_DEVICES))
152 except vol.Invalid
as err:
153 _LOGGER.error(
"Configuration error in %s: %s", YAML_DEVICES,
str(err))
157 await config_entry_oauth2_flow.async_get_config_entry_implementation(
161 session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
166 if session.token[
"expires_at"] >= (now +
timedelta(days=365)).timestamp():
167 session.token[
"expires_in"] = 0
168 session.token[
"expires_at"] = now.timestamp()
170 await session.async_ensure_token_valid()
171 except aiohttp.ClientResponseError
as err:
172 if 400 <= err.status < 500:
173 raise ConfigEntryAuthFailed
from err
174 raise ConfigEntryNotReady
from err
175 except aiohttp.ClientError
as err:
176 raise ConfigEntryNotReady
from err
180 "Required scopes are not available, reauth required"
182 calendar_service = GoogleCalendarService(
185 hass.data[DOMAIN][entry.entry_id][DATA_SERVICE] = calendar_service
190 if entry.unique_id
is None:
192 primary_calendar = await calendar_service.async_get_calendar(
"primary")
193 except AuthException
as err:
194 raise ConfigEntryAuthFailed
from err
195 except ApiException
as err:
196 raise ConfigEntryNotReady
from err
198 hass.config_entries.async_update_entry(entry, unique_id=primary_calendar.id)
204 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
206 entry.async_on_unload(entry.add_update_listener(async_reload_entry))
212 """Verify that the config entry desired scope is present in the oauth token."""
214 token_scopes = entry.data.get(
"token", {}).
get(
"scope", [])
215 return access.scope
in token_scopes
219 """Unload a config entry."""
220 if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
221 hass.data[DOMAIN].pop(entry.entry_id)
226 """Reload config entry if the access options change."""
228 await hass.config_entries.async_reload(entry.entry_id)
232 """Handle removal of a local storage."""
234 await store.async_remove()
239 calendar_service: GoogleCalendarService,
241 """Add the service to add events."""
243 async
def _add_event(call: ServiceCall) ->
None:
244 """Add a new event to calendar."""
246 "The Google Calendar add_event service has been deprecated, and "
247 "will be removed in a future Home Assistant release. Please move "
248 "calls to the create_event service"
251 start: DateOrDatetime |
None =
None
252 end: DateOrDatetime |
None =
None
254 if EVENT_IN
in call.data:
255 if EVENT_IN_DAYS
in call.data[EVENT_IN]:
258 start_in = now +
timedelta(days=call.data[EVENT_IN][EVENT_IN_DAYS])
261 start = DateOrDatetime(date=start_in)
262 end = DateOrDatetime(date=end_in)
264 elif EVENT_IN_WEEKS
in call.data[EVENT_IN]:
267 start_in = now +
timedelta(weeks=call.data[EVENT_IN][EVENT_IN_WEEKS])
270 start = DateOrDatetime(date=start_in)
271 end = DateOrDatetime(date=end_in)
273 elif EVENT_START_DATE
in call.data
and EVENT_END_DATE
in call.data:
274 start = DateOrDatetime(date=call.data[EVENT_START_DATE])
275 end = DateOrDatetime(date=call.data[EVENT_END_DATE])
277 elif EVENT_START_DATETIME
in call.data
and EVENT_END_DATETIME
in call.data:
278 start_dt = call.data[EVENT_START_DATETIME]
279 end_dt = call.data[EVENT_END_DATETIME]
280 start = DateOrDatetime(
281 date_time=start_dt, timezone=
str(hass.config.time_zone)
283 end = DateOrDatetime(date_time=end_dt, timezone=
str(hass.config.time_zone))
285 if start
is None or end
is None:
287 "Missing required fields to set start or end date/datetime"
290 summary=call.data[EVENT_SUMMARY],
291 description=call.data[EVENT_DESCRIPTION],
295 if location := call.data.get(EVENT_LOCATION):
296 event.location = location
298 await calendar_service.async_create_event(
299 call.data[EVENT_CALENDAR_ID],
302 except ApiException
as err:
305 hass.services.async_register(
306 DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA
311 hass: HomeAssistant, calendar: Mapping[str, Any]
313 """Convert data from Google into DEVICE_SCHEMA."""
316 CONF_CAL_ID: calendar[
"id"],
319 CONF_NAME: calendar[
"summary"],
321 "{}", calendar[
"summary"], hass=hass
331 """Load the google_calendar_devices.yaml."""
334 with open(path, encoding=
"utf8")
as file:
335 data = yaml.safe_load(file)
or []
336 for calendar
in data:
338 except FileNotFoundError
as err:
339 _LOGGER.debug(
"Error reading calendar configuration: %s", err)
347 """Write the google_calendar_devices.yaml."""
349 with open(path,
"a", encoding=
"utf8")
as out:
351 yaml.dump([calendar], out, default_flow_style=
False)
352 except FileNotFoundError
as err:
353 _LOGGER.debug(
"Error persisting calendar configuration: %s", err)
web.Response get(self, web.Request request, str config_key)
FeatureAccess get_feature_access(ConfigEntry config_entry)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
None async_remove_entry(HomeAssistant hass, ConfigEntry entry)
None async_reload_entry(HomeAssistant hass, ConfigEntry entry)
dict[str, Any] get_calendar_info(HomeAssistant hass, Mapping[str, Any] calendar)
None update_config(str path, dict[str, Any] calendar)
None async_setup_add_event_service(HomeAssistant hass, GoogleCalendarService calendar_service)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
dict[str, Any] load_config(str path)
bool async_entry_has_scopes(ConfigEntry entry)
None open(self, **Any kwargs)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
str generate_entity_id(str entity_id_format, str|None name, list[str]|None current_ids=None, HomeAssistant|None hass=None)