Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Calendar event device sensors."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Iterable
6 import dataclasses
7 import datetime
8 from http import HTTPStatus
9 from itertools import groupby
10 import logging
11 import re
12 from typing import Any, Final, cast, final
13 
14 from aiohttp import web
15 from dateutil.rrule import rrulestr
16 import voluptuous as vol
17 
18 from homeassistant.components import frontend, http, websocket_api
20  ERR_NOT_FOUND,
21  ERR_NOT_SUPPORTED,
22  ActiveConnection,
23 )
24 from homeassistant.config_entries import ConfigEntry
25 from homeassistant.const import STATE_OFF, STATE_ON
26 from homeassistant.core import (
27  CALLBACK_TYPE,
28  HomeAssistant,
29  ServiceCall,
30  ServiceResponse,
31  SupportsResponse,
32  callback,
33 )
34 from homeassistant.exceptions import HomeAssistantError
35 from homeassistant.helpers import config_validation as cv
36 from homeassistant.helpers.entity import Entity, EntityDescription
37 from homeassistant.helpers.entity_component import EntityComponent
38 from homeassistant.helpers.event import async_track_point_in_time
39 from homeassistant.helpers.template import DATE_STR_FORMAT
40 from homeassistant.helpers.typing import ConfigType
41 from homeassistant.util import dt as dt_util
42 from homeassistant.util.json import JsonValueType
43 
44 from .const import (
45  CONF_EVENT,
46  DATA_COMPONENT,
47  DOMAIN,
48  EVENT_DESCRIPTION,
49  EVENT_DURATION,
50  EVENT_END,
51  EVENT_END_DATE,
52  EVENT_END_DATETIME,
53  EVENT_IN,
54  EVENT_IN_DAYS,
55  EVENT_IN_WEEKS,
56  EVENT_LOCATION,
57  EVENT_RECURRENCE_ID,
58  EVENT_RECURRENCE_RANGE,
59  EVENT_RRULE,
60  EVENT_START,
61  EVENT_START_DATE,
62  EVENT_START_DATETIME,
63  EVENT_SUMMARY,
64  EVENT_TIME_FIELDS,
65  EVENT_TYPES,
66  EVENT_UID,
67  LIST_EVENT_FIELDS,
68  CalendarEntityFeature,
69 )
70 
71 # mypy: disallow-any-generics
72 
73 _LOGGER = logging.getLogger(__name__)
74 
75 ENTITY_ID_FORMAT = DOMAIN + ".{}"
76 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
77 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
78 SCAN_INTERVAL = datetime.timedelta(seconds=60)
79 
80 # Don't support rrules more often than daily
81 VALID_FREQS = {"DAILY", "WEEKLY", "MONTHLY", "YEARLY"}
82 
83 # Ensure events created in Home Assistant have a positive duration
84 MIN_NEW_EVENT_DURATION = datetime.timedelta(seconds=1)
85 
86 # Events must have a non-negative duration e.g. Google Calendar can create zero
87 # duration events in the UI.
88 MIN_EVENT_DURATION = datetime.timedelta(seconds=0)
89 
90 
91 def _has_timezone(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]:
92  """Assert that all datetime values have a timezone."""
93 
94  def validate(obj: dict[str, Any]) -> dict[str, Any]:
95  """Validate that all datetime values have a timezone."""
96  for k in keys:
97  if (
98  (value := obj.get(k))
99  and isinstance(value, datetime.datetime)
100  and value.tzinfo is None
101  ):
102  raise vol.Invalid("Expected all values to have a timezone")
103  return obj
104 
105  return validate
106 
107 
108 def _has_consistent_timezone(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]:
109  """Verify that all datetime values have a consistent timezone."""
110 
111  def validate(obj: dict[str, Any]) -> dict[str, Any]:
112  """Test that all keys that are datetime values have the same timezone."""
113  tzinfos = []
114  for key in keys:
115  if not (value := obj.get(key)) or not isinstance(value, datetime.datetime):
116  return obj
117  tzinfos.append(value.tzinfo)
118  uniq_values = groupby(tzinfos)
119  if len(list(uniq_values)) > 1:
120  raise vol.Invalid("Expected all values to have the same timezone")
121  return obj
122 
123  return validate
124 
125 
126 def _as_local_timezone(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]:
127  """Convert all datetime values to the local timezone."""
128 
129  def validate(obj: dict[str, Any]) -> dict[str, Any]:
130  """Convert all keys that are datetime values to local timezone."""
131  for k in keys:
132  if (value := obj.get(k)) and isinstance(value, datetime.datetime):
133  obj[k] = dt_util.as_local(value)
134  return obj
135 
136  return validate
137 
138 
140  start_key: str, end_key: str, min_duration: datetime.timedelta
141 ) -> Callable[[dict[str, Any]], dict[str, Any]]:
142  """Verify that the time span between start and end has a minimum duration."""
143 
144  def validate(obj: dict[str, Any]) -> dict[str, Any]:
145  if (start := obj.get(start_key)) and (end := obj.get(end_key)):
146  duration = end - start
147  if duration < min_duration:
148  raise vol.Invalid(
149  f"Expected minimum event duration of {min_duration} ({start}, {end})"
150  )
151  return obj
152 
153  return validate
154 
155 
156 def _has_same_type(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]:
157  """Verify that all values are of the same type."""
158 
159  def validate(obj: dict[str, Any]) -> dict[str, Any]:
160  """Test that all keys in the dict have values of the same type."""
161  uniq_values = groupby(type(obj[k]) for k in keys)
162  if len(list(uniq_values)) > 1:
163  raise vol.Invalid(f"Expected all values to be the same type: {keys}")
164  return obj
165 
166  return validate
167 
168 
169 def _validate_rrule(value: Any) -> str:
170  """Validate a recurrence rule string."""
171  if value is None:
172  raise vol.Invalid("rrule value is None")
173 
174  if not isinstance(value, str):
175  raise vol.Invalid("rrule value expected a string")
176 
177  try:
178  rrulestr(value)
179  except ValueError as err:
180  raise vol.Invalid(f"Invalid rrule '{value}': {err}") from err
181 
182  # Example format: FREQ=DAILY;UNTIL=...
183  rule_parts = dict(s.split("=", 1) for s in value.split(";"))
184  if not (freq := rule_parts.get("FREQ")):
185  raise vol.Invalid("rrule did not contain FREQ")
186 
187  if freq not in VALID_FREQS:
188  raise vol.Invalid(f"Invalid frequency for rule: {value}")
189 
190  return str(value)
191 
192 
193 def _empty_as_none(value: str | None) -> str | None:
194  """Convert any empty string values to None."""
195  return value or None
196 
197 
198 CREATE_EVENT_SERVICE = "create_event"
199 CREATE_EVENT_SCHEMA = vol.All(
200  cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
201  cv.has_at_most_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
202  cv.make_entity_service_schema(
203  {
204  vol.Required(EVENT_SUMMARY): cv.string,
205  vol.Optional(EVENT_DESCRIPTION, default=""): cv.string,
206  vol.Optional(EVENT_LOCATION): cv.string,
207  vol.Inclusive(
208  EVENT_START_DATE, "dates", "Start and end dates must both be specified"
209  ): cv.date,
210  vol.Inclusive(
211  EVENT_END_DATE, "dates", "Start and end dates must both be specified"
212  ): cv.date,
213  vol.Inclusive(
214  EVENT_START_DATETIME,
215  "datetimes",
216  "Start and end datetimes must both be specified",
217  ): cv.datetime,
218  vol.Inclusive(
219  EVENT_END_DATETIME,
220  "datetimes",
221  "Start and end datetimes must both be specified",
222  ): cv.datetime,
223  vol.Optional(EVENT_IN): vol.Schema(
224  {
225  vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES): cv.positive_int,
226  vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES): cv.positive_int,
227  }
228  ),
229  },
230  ),
231  _has_consistent_timezone(EVENT_START_DATETIME, EVENT_END_DATETIME),
232  _as_local_timezone(EVENT_START_DATETIME, EVENT_END_DATETIME),
233  _has_min_duration(EVENT_START_DATE, EVENT_END_DATE, MIN_NEW_EVENT_DURATION),
234  _has_min_duration(EVENT_START_DATETIME, EVENT_END_DATETIME, MIN_NEW_EVENT_DURATION),
235 )
236 
237 WEBSOCKET_EVENT_SCHEMA = vol.Schema(
238  vol.All(
239  {
240  vol.Required(EVENT_START): vol.Any(cv.date, cv.datetime),
241  vol.Required(EVENT_END): vol.Any(cv.date, cv.datetime),
242  vol.Required(EVENT_SUMMARY): cv.string,
243  vol.Optional(EVENT_DESCRIPTION): cv.string,
244  vol.Optional(EVENT_LOCATION): cv.string,
245  vol.Optional(EVENT_RRULE): _validate_rrule,
246  },
247  _has_same_type(EVENT_START, EVENT_END),
248  _has_consistent_timezone(EVENT_START, EVENT_END),
249  _as_local_timezone(EVENT_START, EVENT_END),
250  _has_min_duration(EVENT_START, EVENT_END, MIN_NEW_EVENT_DURATION),
251  )
252 )
253 
254 # Validation for the CalendarEvent dataclass
255 CALENDAR_EVENT_SCHEMA = vol.Schema(
256  vol.All(
257  {
258  vol.Required("start"): vol.Any(cv.date, cv.datetime),
259  vol.Required("end"): vol.Any(cv.date, cv.datetime),
260  vol.Required(EVENT_SUMMARY): cv.string,
261  vol.Optional(EVENT_RRULE): _validate_rrule,
262  },
263  _has_same_type("start", "end"),
264  _has_timezone("start", "end"),
265  _as_local_timezone("start", "end"),
266  _has_min_duration("start", "end", MIN_EVENT_DURATION),
267  ),
268  extra=vol.ALLOW_EXTRA,
269 )
270 
271 SERVICE_GET_EVENTS: Final = "get_events"
272 SERVICE_GET_EVENTS_SCHEMA: Final = vol.All(
273  cv.has_at_least_one_key(EVENT_END_DATETIME, EVENT_DURATION),
274  cv.has_at_most_one_key(EVENT_END_DATETIME, EVENT_DURATION),
275  cv.make_entity_service_schema(
276  {
277  vol.Optional(EVENT_START_DATETIME): cv.datetime,
278  vol.Optional(EVENT_END_DATETIME): cv.datetime,
279  vol.Optional(EVENT_DURATION): vol.All(
280  cv.time_period, cv.positive_timedelta
281  ),
282  }
283  ),
284 )
285 
286 
287 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
288  """Track states and offer events for calendars."""
289  component = hass.data[DATA_COMPONENT] = EntityComponent[CalendarEntity](
290  _LOGGER, DOMAIN, hass, SCAN_INTERVAL
291  )
292 
293  hass.http.register_view(CalendarListView(component))
294  hass.http.register_view(CalendarEventView(component))
295 
296  frontend.async_register_built_in_panel(
297  hass, "calendar", "calendar", "hass:calendar"
298  )
299 
300  websocket_api.async_register_command(hass, handle_calendar_event_create)
301  websocket_api.async_register_command(hass, handle_calendar_event_delete)
302  websocket_api.async_register_command(hass, handle_calendar_event_update)
303 
304  component.async_register_entity_service(
305  CREATE_EVENT_SERVICE,
306  CREATE_EVENT_SCHEMA,
307  async_create_event,
308  required_features=[CalendarEntityFeature.CREATE_EVENT],
309  )
310  component.async_register_entity_service(
311  SERVICE_GET_EVENTS,
312  SERVICE_GET_EVENTS_SCHEMA,
313  async_get_events_service,
314  supports_response=SupportsResponse.ONLY,
315  )
316  await component.async_setup(config)
317  return True
318 
319 
320 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
321  """Set up a config entry."""
322  return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
323 
324 
325 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
326  """Unload a config entry."""
327  return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
328 
329 
330 def get_date(date: dict[str, Any]) -> datetime.datetime:
331  """Get the dateTime from date or dateTime as a local."""
332  if "date" in date:
333  parsed_date = dt_util.parse_date(date["date"])
334  assert parsed_date
335  return dt_util.start_of_local_day(
336  datetime.datetime.combine(parsed_date, datetime.time.min)
337  )
338  parsed_datetime = dt_util.parse_datetime(date["dateTime"])
339  assert parsed_datetime
340  return dt_util.as_local(parsed_datetime)
341 
342 
343 @dataclasses.dataclass
345  """An event on a calendar."""
346 
347  start: datetime.date | datetime.datetime
348  end: datetime.date | datetime.datetime
349  summary: str
350  description: str | None = None
351  location: str | None = None
352 
353  uid: str | None = None
354  recurrence_id: str | None = None
355  rrule: str | None = None
356 
357  @property
358  def start_datetime_local(self) -> datetime.datetime:
359  """Return event start time as a local datetime."""
360  return _get_datetime_local(self.startstart)
361 
362  @property
363  def end_datetime_local(self) -> datetime.datetime:
364  """Return event end time as a local datetime."""
365  return _get_datetime_local(self.endend)
366 
367  @property
368  def all_day(self) -> bool:
369  """Return true if the event is an all day event."""
370  return not isinstance(self.startstart, datetime.datetime)
371 
372  def as_dict(self) -> dict[str, Any]:
373  """Return a dict representation of the event."""
374  return {
375  **dataclasses.asdict(self, dict_factory=_event_dict_factory),
376  "all_day": self.all_dayall_day,
377  }
378 
379  def __post_init__(self) -> None:
380  """Perform validation on the CalendarEvent."""
381 
382  def skip_none(obj: Iterable[tuple[str, Any]]) -> dict[str, str]:
383  return {k: v for k, v in obj if v is not None}
384 
385  try:
386  CALENDAR_EVENT_SCHEMA(dataclasses.asdict(self, dict_factory=skip_none))
387  except vol.Invalid as err:
388  raise HomeAssistantError(
389  f"Failed to validate CalendarEvent: {err}"
390  ) from err
391 
392  # It is common to set a start an end date to be the same thing for
393  # an all day event, but that is not a valid duration. Fix to have a
394  # duration of one day.
395  if (
396  not isinstance(self.startstart, datetime.datetime)
397  and not isinstance(self.endend, datetime.datetime)
398  and self.startstart == self.endend
399  ):
400  self.endend = self.startstart + datetime.timedelta(days=1)
401 
402 
403 def _event_dict_factory(obj: Iterable[tuple[str, Any]]) -> dict[str, str]:
404  """Convert CalendarEvent dataclass items to dictionary of attributes."""
405  result: dict[str, str] = {}
406  for name, value in obj:
407  if isinstance(value, (datetime.datetime, datetime.date)):
408  result[name] = value.isoformat()
409  elif value is not None:
410  result[name] = str(value)
411  return result
412 
413 
414 def _api_event_dict_factory(obj: Iterable[tuple[str, Any]]) -> dict[str, Any]:
415  """Convert CalendarEvent dataclass items to the API format."""
416  result: dict[str, Any] = {}
417  for name, value in obj:
418  if isinstance(value, datetime.datetime):
419  result[name] = {"dateTime": dt_util.as_local(value).isoformat()}
420  elif isinstance(value, datetime.date):
421  result[name] = {"date": value.isoformat()}
422  else:
423  result[name] = value
424  return result
425 
426 
428  obj: Iterable[tuple[str, Any]],
429 ) -> dict[str, JsonValueType]:
430  """Convert CalendarEvent dataclass items to dictionary of attributes."""
431  return {
432  name: value
433  for name, value in _event_dict_factory(obj).items()
434  if name in LIST_EVENT_FIELDS and value is not None
435  }
436 
437 
439  dt_or_d: datetime.datetime | datetime.date,
440 ) -> datetime.datetime:
441  """Convert a calendar event date/datetime to a datetime if needed."""
442  if isinstance(dt_or_d, datetime.datetime):
443  return dt_util.as_local(dt_or_d)
444  return dt_util.start_of_local_day(dt_or_d)
445 
446 
447 def _get_api_date(dt_or_d: datetime.datetime | datetime.date) -> dict[str, str]:
448  """Convert a calendar event date/datetime to a datetime if needed."""
449  if isinstance(dt_or_d, datetime.datetime):
450  return {"dateTime": dt_util.as_local(dt_or_d).isoformat()}
451  return {"date": dt_or_d.isoformat()}
452 
453 
454 def extract_offset(summary: str, offset_prefix: str) -> tuple[str, datetime.timedelta]:
455  """Extract the offset from the event summary.
456 
457  Return a tuple with the updated event summary and offset time.
458  """
459  # check if we have an offset tag in the message
460  # time is HH:MM or MM
461  reg = f"{offset_prefix}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)"
462  search = re.search(reg, summary)
463  if search and search.group(1):
464  time = search.group(1)
465  if ":" not in time:
466  if time[0] in ("+", "-"):
467  time = f"{time[0]}0:{time[1:]}"
468  else:
469  time = f"0:{time}"
470 
471  offset_time = cv.time_period_str(time)
472  summary = (summary[: search.start()] + summary[search.end() :]).strip()
473  return (summary, offset_time)
474  return (summary, datetime.timedelta())
475 
476 
478  start: datetime.datetime, offset_time: datetime.timedelta
479 ) -> bool:
480  """Have we reached the offset time specified in the event title."""
481  if offset_time == datetime.timedelta():
482  return False
483  return start + offset_time <= dt_util.now(start.tzinfo)
484 
485 
486 class CalendarEntityDescription(EntityDescription, frozen_or_thawed=True):
487  """A class that describes calendar entities."""
488 
489 
491  """Base class for calendar event entities."""
492 
493  entity_description: CalendarEntityDescription
494 
495  _entity_component_unrecorded_attributes = frozenset({"description"})
496 
497  _alarm_unsubs: list[CALLBACK_TYPE] | None = None
498 
499  @property
500  def event(self) -> CalendarEvent | None:
501  """Return the next upcoming event."""
502  raise NotImplementedError
503 
504  @final
505  @property
506  def state_attributes(self) -> dict[str, Any] | None:
507  """Return the entity state attributes."""
508  if (event := self.eventevent) is None:
509  return None
510 
511  return {
512  "message": event.summary,
513  "all_day": event.all_day,
514  "start_time": event.start_datetime_local.strftime(DATE_STR_FORMAT),
515  "end_time": event.end_datetime_local.strftime(DATE_STR_FORMAT),
516  "location": event.location if event.location else "",
517  "description": event.description if event.description else "",
518  }
519 
520  @final
521  @property
522  def state(self) -> str:
523  """Return the state of the calendar event."""
524  if (event := self.eventevent) is None:
525  return STATE_OFF
526 
527  now = dt_util.now()
528 
529  if event.start_datetime_local <= now < event.end_datetime_local:
530  return STATE_ON
531 
532  return STATE_OFF
533 
534  @callback
535  def async_write_ha_state(self) -> None:
536  """Write the state to the state machine.
537 
538  This sets up listeners to handle state transitions for start or end of
539  the current or upcoming event.
540  """
541  super().async_write_ha_state()
542  if self._alarm_unsubs_alarm_unsubs is None:
543  self._alarm_unsubs_alarm_unsubs = []
544  _LOGGER.debug(
545  "Clearing %s alarms (%s)", self.entity_identity_id, len(self._alarm_unsubs_alarm_unsubs)
546  )
547  for unsub in self._alarm_unsubs_alarm_unsubs:
548  unsub()
549  self._alarm_unsubs_alarm_unsubs.clear()
550 
551  now = dt_util.now()
552  event = self.eventevent
553  if event is None or now >= event.end_datetime_local:
554  _LOGGER.debug("No alarms needed for %s (event=%s)", self.entity_identity_id, event)
555  return
556 
557  @callback
558  def update(_: datetime.datetime) -> None:
559  """Update state and reschedule next alarms."""
560  _LOGGER.debug("Running %s update", self.entity_identity_id)
561  self.async_write_ha_stateasync_write_ha_stateasync_write_ha_state()
562 
563  if now < event.start_datetime_local:
564  self._alarm_unsubs_alarm_unsubs.append(
566  self.hasshass,
567  update,
568  event.start_datetime_local,
569  )
570  )
571  self._alarm_unsubs_alarm_unsubs.append(
572  async_track_point_in_time(self.hasshass, update, event.end_datetime_local)
573  )
574  _LOGGER.debug(
575  "Scheduled %d updates for %s (%s, %s)",
576  len(self._alarm_unsubs_alarm_unsubs),
577  self.entity_identity_id,
578  event.start_datetime_local,
579  event.end_datetime_local,
580  )
581 
582  async def async_will_remove_from_hass(self) -> None:
583  """Run when entity will be removed from hass.
584 
585  To be extended by integrations.
586  """
587  for unsub in self._alarm_unsubs_alarm_unsubs or ():
588  unsub()
589  self._alarm_unsubs_alarm_unsubs = None
590 
591  async def async_get_events(
592  self,
593  hass: HomeAssistant,
594  start_date: datetime.datetime,
595  end_date: datetime.datetime,
596  ) -> list[CalendarEvent]:
597  """Return calendar events within a datetime range."""
598  raise NotImplementedError
599 
600  async def async_create_event(self, **kwargs: Any) -> None:
601  """Add a new event to calendar."""
602  raise NotImplementedError
603 
605  self,
606  uid: str,
607  recurrence_id: str | None = None,
608  recurrence_range: str | None = None,
609  ) -> None:
610  """Delete an event on the calendar."""
611  raise NotImplementedError
612 
614  self,
615  uid: str,
616  event: dict[str, Any],
617  recurrence_id: str | None = None,
618  recurrence_range: str | None = None,
619  ) -> None:
620  """Delete an event on the calendar."""
621  raise NotImplementedError
622 
623 
624 class CalendarEventView(http.HomeAssistantView):
625  """View to retrieve calendar content."""
626 
627  url = "/api/calendars/{entity_id}"
628  name = "api:calendars:calendar"
629 
630  def __init__(self, component: EntityComponent[CalendarEntity]) -> None:
631  """Initialize calendar view."""
632  self.componentcomponent = component
633 
634  async def get(self, request: web.Request, entity_id: str) -> web.Response:
635  """Return calendar events."""
636  if not (entity := self.componentcomponent.get_entity(entity_id)) or not isinstance(
637  entity, CalendarEntity
638  ):
639  return web.Response(status=HTTPStatus.BAD_REQUEST)
640 
641  start = request.query.get("start")
642  end = request.query.get("end")
643  if start is None or end is None:
644  return web.Response(status=HTTPStatus.BAD_REQUEST)
645  try:
646  start_date = dt_util.parse_datetime(start)
647  end_date = dt_util.parse_datetime(end)
648  except (ValueError, AttributeError):
649  return web.Response(status=HTTPStatus.BAD_REQUEST)
650  if start_date is None or end_date is None:
651  return web.Response(status=HTTPStatus.BAD_REQUEST)
652  if start_date > end_date:
653  return web.Response(status=HTTPStatus.BAD_REQUEST)
654 
655  try:
656  calendar_event_list = await entity.async_get_events(
657  request.app[http.KEY_HASS],
658  dt_util.as_local(start_date),
659  dt_util.as_local(end_date),
660  )
661  except HomeAssistantError as err:
662  _LOGGER.debug("Error reading events: %s", err)
663  return self.json_message(
664  f"Error reading events: {err}", HTTPStatus.INTERNAL_SERVER_ERROR
665  )
666 
667  return self.json(
668  [
669  dataclasses.asdict(event, dict_factory=_api_event_dict_factory)
670  for event in calendar_event_list
671  ]
672  )
673 
674 
675 class CalendarListView(http.HomeAssistantView):
676  """View to retrieve calendar list."""
677 
678  url = "/api/calendars"
679  name = "api:calendars"
680 
681  def __init__(self, component: EntityComponent[CalendarEntity]) -> None:
682  """Initialize calendar view."""
683  self.componentcomponent = component
684 
685  async def get(self, request: web.Request) -> web.Response:
686  """Retrieve calendar list."""
687  hass = request.app[http.KEY_HASS]
688  calendar_list: list[dict[str, str]] = []
689 
690  for entity in self.componentcomponent.entities:
691  state = hass.states.get(entity.entity_id)
692  assert state
693  calendar_list.append({"name": state.name, "entity_id": entity.entity_id})
694 
695  return self.json(sorted(calendar_list, key=lambda x: cast(str, x["name"])))
696 
697 
698 @websocket_api.websocket_command( { vol.Required("type"): "calendar/event/create",
699  vol.Required("entity_id"): cv.entity_id,
700  CONF_EVENT: WEBSOCKET_EVENT_SCHEMA,
701  }
702 )
703 @websocket_api.async_response
705  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
706 ) -> None:
707  """Handle creation of a calendar event."""
708  if not (entity := hass.data[DATA_COMPONENT].get_entity(msg["entity_id"])):
709  connection.send_error(msg["id"], ERR_NOT_FOUND, "Entity not found")
710  return
711 
712  if (
713  not entity.supported_features
714  or not entity.supported_features & CalendarEntityFeature.CREATE_EVENT
715  ):
716  connection.send_message(
717  websocket_api.error_message(
718  msg["id"], ERR_NOT_SUPPORTED, "Calendar does not support event creation"
719  )
720  )
721  return
722 
723  try:
724  await entity.async_create_event(**msg[CONF_EVENT])
725  except HomeAssistantError as ex:
726  connection.send_error(msg["id"], "failed", str(ex))
727  else:
728  connection.send_result(msg["id"])
729 
730 
731 @websocket_api.websocket_command( { vol.Required("type"): "calendar/event/delete",
732  vol.Required("entity_id"): cv.entity_id,
733  vol.Required(EVENT_UID): cv.string,
734  vol.Optional(EVENT_RECURRENCE_ID): vol.Any(
735  vol.All(cv.string, _empty_as_none), None
736  ),
737  vol.Optional(EVENT_RECURRENCE_RANGE): cv.string,
738  }
739 )
740 @websocket_api.async_response
742  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
743 ) -> None:
744  """Handle delete of a calendar event."""
745 
746  if not (entity := hass.data[DATA_COMPONENT].get_entity(msg["entity_id"])):
747  connection.send_error(msg["id"], ERR_NOT_FOUND, "Entity not found")
748  return
749 
750  if (
751  not entity.supported_features
752  or not entity.supported_features & CalendarEntityFeature.DELETE_EVENT
753  ):
754  connection.send_message(
755  websocket_api.error_message(
756  msg["id"], ERR_NOT_SUPPORTED, "Calendar does not support event deletion"
757  )
758  )
759  return
760 
761  try:
762  await entity.async_delete_event(
763  msg[EVENT_UID],
764  recurrence_id=msg.get(EVENT_RECURRENCE_ID),
765  recurrence_range=msg.get(EVENT_RECURRENCE_RANGE),
766  )
767  except (HomeAssistantError, ValueError) as ex:
768  _LOGGER.error("Error handling Calendar Event call: %s", ex)
769  connection.send_error(msg["id"], "failed", str(ex))
770  else:
771  connection.send_result(msg["id"])
772 
773 
774 @websocket_api.websocket_command( { vol.Required("type"): "calendar/event/update",
775  vol.Required("entity_id"): cv.entity_id,
776  vol.Required(EVENT_UID): cv.string,
777  vol.Optional(EVENT_RECURRENCE_ID): vol.Any(
778  vol.All(cv.string, _empty_as_none), None
779  ),
780  vol.Optional(EVENT_RECURRENCE_RANGE): cv.string,
781  vol.Required(CONF_EVENT): WEBSOCKET_EVENT_SCHEMA,
782  }
783 )
784 @websocket_api.async_response
786  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
787 ) -> None:
788  """Handle creation of a calendar event."""
789  if not (entity := hass.data[DATA_COMPONENT].get_entity(msg["entity_id"])):
790  connection.send_error(msg["id"], ERR_NOT_FOUND, "Entity not found")
791  return
792 
793  if (
794  not entity.supported_features
795  or not entity.supported_features & CalendarEntityFeature.UPDATE_EVENT
796  ):
797  connection.send_message(
798  websocket_api.error_message(
799  msg["id"], ERR_NOT_SUPPORTED, "Calendar does not support event update"
800  )
801  )
802  return
803 
804  try:
805  await entity.async_update_event(
806  msg[EVENT_UID],
807  msg[CONF_EVENT],
808  recurrence_id=msg.get(EVENT_RECURRENCE_ID),
809  recurrence_range=msg.get(EVENT_RECURRENCE_RANGE),
810  )
811  except (HomeAssistantError, ValueError) as ex:
812  _LOGGER.error("Error handling Calendar Event call: %s", ex)
813  connection.send_error(msg["id"], "failed", str(ex))
814  else:
815  connection.send_result(msg["id"])
816 
817 
819  values: dict[str, Any],
820 ) -> tuple[datetime.datetime | datetime.date, datetime.datetime | datetime.date]:
821  """Parse a create event service call and convert the args ofr a create event entity call.
822 
823  This converts the input service arguments into a `start` and `end` date or date time. This
824  exists because service calls use `start_date` and `start_date_time` whereas the
825  normal entity methods can take either a `datetime` or `date` as a single `start` argument.
826  It also handles the other service call variations like "in days" as well.
827  """
828 
829  if event_in := values.get(EVENT_IN):
830  days = event_in.get(EVENT_IN_DAYS, 7 * event_in.get(EVENT_IN_WEEKS, 0))
831  today = datetime.date.today()
832  return (
833  today + datetime.timedelta(days=days),
834  today + datetime.timedelta(days=days + 1),
835  )
836 
837  if EVENT_START_DATE in values and EVENT_END_DATE in values:
838  return (values[EVENT_START_DATE], values[EVENT_END_DATE])
839 
840  if EVENT_START_DATETIME in values and EVENT_END_DATETIME in values:
841  return (values[EVENT_START_DATETIME], values[EVENT_END_DATETIME])
842 
843  raise ValueError("Missing required fields to set start or end date/datetime")
844 
845 
846 async def async_create_event(entity: CalendarEntity, call: ServiceCall) -> None:
847  """Add a new event to calendar."""
848  # Convert parameters to format used by async_create_event
849  (start, end) = _validate_timespan(call.data)
850  params = {
851  **{k: v for k, v in call.data.items() if k not in EVENT_TIME_FIELDS},
852  EVENT_START: start,
853  EVENT_END: end,
854  }
855  await entity.async_create_event(**params)
856 
857 
858 async def async_get_events_service(
859  calendar: CalendarEntity, service_call: ServiceCall
860 ) -> ServiceResponse:
861  """List events on a calendar during a time range."""
862  start = service_call.data.get(EVENT_START_DATETIME, dt_util.now())
863  if EVENT_DURATION in service_call.data:
864  end = start + service_call.data[EVENT_DURATION]
865  else:
866  end = service_call.data[EVENT_END_DATETIME]
867  calendar_event_list = await calendar.async_get_events(
868  calendar.hass, dt_util.as_local(start), dt_util.as_local(end)
869  )
870  return {
871  "events": [
872  dataclasses.asdict(event, dict_factory=_list_events_dict_factory)
873  for event in calendar_event_list
874  ]
875  }
876 
None async_create_event(self, **Any kwargs)
Definition: __init__.py:600
None async_update_event(self, str uid, dict[str, Any] event, str|None recurrence_id=None, str|None recurrence_range=None)
Definition: __init__.py:619
None async_delete_event(self, str uid, str|None recurrence_id=None, str|None recurrence_range=None)
Definition: __init__.py:609
dict[str, Any]|None state_attributes(self)
Definition: __init__.py:506
list[CalendarEvent] async_get_events(self, HomeAssistant hass, datetime.datetime start_date, datetime.datetime end_date)
Definition: __init__.py:596
None __init__(self, EntityComponent[CalendarEntity] component)
Definition: __init__.py:630
web.Response get(self, web.Request request, str entity_id)
Definition: __init__.py:634
datetime.datetime start_datetime_local(self)
Definition: __init__.py:358
datetime.datetime end_datetime_local(self)
Definition: __init__.py:363
None __init__(self, EntityComponent[CalendarEntity] component)
Definition: __init__.py:681
web.Response get(self, web.Request request)
Definition: __init__.py:685
CalendarEntity get_entity(HomeAssistant hass, str entity_id)
Definition: trigger.py:96
None handle_calendar_event_update(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:793
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:320
Callable[[dict[str, Any]], dict[str, Any]] _as_local_timezone(*Any keys)
Definition: __init__.py:126
ServiceResponse async_get_events_service(CalendarEntity calendar, ServiceCall service_call)
Definition: __init__.py:866
Callable[[dict[str, Any]], dict[str, Any]] _has_min_duration(str start_key, str end_key, datetime.timedelta min_duration)
Definition: __init__.py:141
None handle_calendar_event_delete(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:747
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:325
Callable[[dict[str, Any]], dict[str, Any]] _has_same_type(*Any keys)
Definition: __init__.py:156
Callable[[dict[str, Any]], dict[str, Any]] _has_consistent_timezone(*Any keys)
Definition: __init__.py:108
str|None _empty_as_none(str|None value)
Definition: __init__.py:193
tuple[str, datetime.timedelta] extract_offset(str summary, str offset_prefix)
Definition: __init__.py:454
datetime.datetime get_date(dict[str, Any] date)
Definition: __init__.py:330
bool is_offset_reached(datetime.datetime start, datetime.timedelta offset_time)
Definition: __init__.py:479
dict[str, JsonValueType] _list_events_dict_factory(Iterable[tuple[str, Any]] obj)
Definition: __init__.py:429
dict[str, Any] _api_event_dict_factory(Iterable[tuple[str, Any]] obj)
Definition: __init__.py:414
datetime.datetime _get_datetime_local(datetime.datetime|datetime.date dt_or_d)
Definition: __init__.py:440
tuple[datetime.datetime|datetime.date, datetime.datetime|datetime.date] _validate_timespan(dict[str, Any] values)
Definition: __init__.py:826
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:287
Callable[[dict[str, Any]], dict[str, Any]] _has_timezone(*Any keys)
Definition: __init__.py:91
None async_create_event(CalendarEntity entity, ServiceCall call)
Definition: __init__.py:852
None handle_calendar_event_create(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:708
dict[str, str] _event_dict_factory(Iterable[tuple[str, Any]] obj)
Definition: __init__.py:403
dict[str, str] _get_api_date(datetime.datetime|datetime.date dt_or_d)
Definition: __init__.py:447
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
dict[str, Any] validate(SchemaCommonFlowHandler handler, dict[str, Any] user_input)
Definition: config_flow.py:27
CALLBACK_TYPE async_track_point_in_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)
Definition: event.py:1462