Home Assistant Unofficial Reference 2024.12.1
calendar.py
Go to the documentation of this file.
1 """Calendar platform for a Local Calendar."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from datetime import date, datetime, timedelta
7 import logging
8 from typing import Any
9 
10 from ical.calendar import Calendar
11 from ical.calendar_stream import IcsCalendarStream
12 from ical.event import Event
13 from ical.exceptions import CalendarParseError
14 from ical.store import EventStore, EventStoreError
15 from ical.types import Range, Recur
16 import voluptuous as vol
17 
19  EVENT_END,
20  EVENT_RRULE,
21  EVENT_START,
22  CalendarEntity,
23  CalendarEntityFeature,
24  CalendarEvent,
25 )
26 from homeassistant.config_entries import ConfigEntry
27 from homeassistant.core import HomeAssistant
28 from homeassistant.exceptions import HomeAssistantError
29 from homeassistant.helpers.entity_platform import AddEntitiesCallback
30 from homeassistant.util import dt as dt_util
31 
32 from .const import CONF_CALENDAR_NAME, DOMAIN
33 from .store import LocalCalendarStore
34 
35 _LOGGER = logging.getLogger(__name__)
36 
37 PRODID = "-//homeassistant.io//local_calendar 1.0//EN"
38 
39 
41  hass: HomeAssistant,
42  config_entry: ConfigEntry,
43  async_add_entities: AddEntitiesCallback,
44 ) -> None:
45  """Set up the local calendar platform."""
46  store = hass.data[DOMAIN][config_entry.entry_id]
47  ics = await store.async_load()
48  calendar: Calendar = await hass.async_add_executor_job(
49  IcsCalendarStream.calendar_from_ics, ics
50  )
51  calendar.prodid = PRODID
52 
53  name = config_entry.data[CONF_CALENDAR_NAME]
54  entity = LocalCalendarEntity(store, calendar, name, unique_id=config_entry.entry_id)
55  async_add_entities([entity], True)
56 
57 
59  """A calendar entity backed by a local iCalendar file."""
60 
61  _attr_has_entity_name = True
62  _attr_supported_features = (
63  CalendarEntityFeature.CREATE_EVENT
64  | CalendarEntityFeature.DELETE_EVENT
65  | CalendarEntityFeature.UPDATE_EVENT
66  )
67 
68  def __init__(
69  self,
70  store: LocalCalendarStore,
71  calendar: Calendar,
72  name: str,
73  unique_id: str,
74  ) -> None:
75  """Initialize LocalCalendarEntity."""
76  self._store_store = store
77  self._calendar_calendar = calendar
78  self._calendar_lock_calendar_lock = asyncio.Lock()
79  self._event_event: CalendarEvent | None = None
80  self._attr_name_attr_name = name
81  self._attr_unique_id_attr_unique_id = unique_id
82 
83  @property
84  def event(self) -> CalendarEvent | None:
85  """Return the next upcoming event."""
86  return self._event_event
87 
88  async def async_get_events(
89  self, hass: HomeAssistant, start_date: datetime, end_date: datetime
90  ) -> list[CalendarEvent]:
91  """Get all events in a specific time frame."""
92  events = self._calendar_calendar.timeline_tz(start_date.tzinfo).overlapping(
93  start_date,
94  end_date,
95  )
96  return [_get_calendar_event(event) for event in events]
97 
98  async def async_update(self) -> None:
99  """Update entity state with the next upcoming event."""
100  now = dt_util.now()
101  events = self._calendar_calendar.timeline_tz(now.tzinfo).active_after(now)
102  if event := next(events, None):
103  self._event_event = _get_calendar_event(event)
104  else:
105  self._event_event = None
106 
107  async def _async_store(self) -> None:
108  """Persist the calendar to disk."""
109  content = IcsCalendarStream.calendar_to_ics(self._calendar_calendar)
110  await self._store_store.async_store(content)
111 
112  async def async_create_event(self, **kwargs: Any) -> None:
113  """Add a new event to calendar."""
114  event = _parse_event(kwargs)
115  async with self._calendar_lock_calendar_lock:
116  event_store = EventStore(self._calendar_calendar)
117  await self.hasshass.async_add_executor_job(event_store.add, event)
118  await self._async_store_async_store()
119  await self.async_update_ha_stateasync_update_ha_state(force_refresh=True)
120 
122  self,
123  uid: str,
124  recurrence_id: str | None = None,
125  recurrence_range: str | None = None,
126  ) -> None:
127  """Delete an event on the calendar."""
128  range_value: Range = Range.NONE
129  if recurrence_range == Range.THIS_AND_FUTURE:
130  range_value = Range.THIS_AND_FUTURE
131  async with self._calendar_lock_calendar_lock:
132  try:
133  EventStore(self._calendar_calendar).delete(
134  uid,
135  recurrence_id=recurrence_id,
136  recurrence_range=range_value,
137  )
138  except EventStoreError as err:
139  raise HomeAssistantError(f"Error while deleting event: {err}") from err
140  await self._async_store_async_store()
141  await self.async_update_ha_stateasync_update_ha_state(force_refresh=True)
142 
144  self,
145  uid: str,
146  event: dict[str, Any],
147  recurrence_id: str | None = None,
148  recurrence_range: str | None = None,
149  ) -> None:
150  """Update an existing event on the calendar."""
151  new_event = _parse_event(event)
152  range_value: Range = Range.NONE
153  if recurrence_range == Range.THIS_AND_FUTURE:
154  range_value = Range.THIS_AND_FUTURE
155 
156  async with self._calendar_lock_calendar_lock:
157  event_store = EventStore(self._calendar_calendar)
158 
159  def apply_edit() -> None:
160  event_store.edit(
161  uid,
162  new_event,
163  recurrence_id=recurrence_id,
164  recurrence_range=range_value,
165  )
166 
167  try:
168  await self.hasshass.async_add_executor_job(apply_edit)
169  except EventStoreError as err:
170  raise HomeAssistantError(f"Error while updating event: {err}") from err
171  await self._async_store_async_store()
172  await self.async_update_ha_stateasync_update_ha_state(force_refresh=True)
173 
174 
175 def _parse_event(event: dict[str, Any]) -> Event:
176  """Parse an ical event from a home assistant event dictionary."""
177  if rrule := event.get(EVENT_RRULE):
178  event[EVENT_RRULE] = Recur.from_rrule(rrule)
179 
180  # This function is called with new events created in the local timezone,
181  # however ical library does not properly return recurrence_ids for
182  # start dates with a timezone. For now, ensure any datetime is stored as a
183  # floating local time to ensure we still apply proper local timezone rules.
184  # This can be removed when ical is updated with a new recurrence_id format
185  # https://github.com/home-assistant/core/issues/87759
186  for key in (EVENT_START, EVENT_END):
187  if (
188  (value := event[key])
189  and isinstance(value, datetime)
190  and value.tzinfo is not None
191  ):
192  event[key] = dt_util.as_local(value).replace(tzinfo=None)
193 
194  try:
195  return Event(**event)
196  except CalendarParseError as err:
197  _LOGGER.debug("Error parsing event input fields: %s (%s)", event, str(err))
198  raise vol.Invalid("Error parsing event input fields") from err
199 
200 
201 def _get_calendar_event(event: Event) -> CalendarEvent:
202  """Return a CalendarEvent from an API event."""
203  start: datetime | date
204  end: datetime | date
205  if isinstance(event.start, datetime) and isinstance(event.end, datetime):
206  start = dt_util.as_local(event.start)
207  end = dt_util.as_local(event.end)
208  if (end - start) <= timedelta(seconds=0):
209  end = start + timedelta(minutes=30)
210  else:
211  start = event.start
212  end = event.end
213  if (end - start) < timedelta(days=0):
214  end = start + timedelta(days=1)
215 
216  return CalendarEvent(
217  summary=event.summary,
218  start=start,
219  end=end,
220  description=event.description,
221  uid=event.uid,
222  rrule=event.rrule.as_rrule_str() if event.rrule else None,
223  recurrence_id=event.recurrence_id,
224  location=event.location,
225  )
None __init__(self, LocalCalendarStore store, Calendar calendar, str name, str unique_id)
Definition: calendar.py:74
list[CalendarEvent] async_get_events(self, HomeAssistant hass, datetime start_date, datetime end_date)
Definition: calendar.py:90
None async_update_event(self, str uid, dict[str, Any] event, str|None recurrence_id=None, str|None recurrence_range=None)
Definition: calendar.py:149
None async_delete_event(self, str uid, str|None recurrence_id=None, str|None recurrence_range=None)
Definition: calendar.py:126
None async_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:942
web.Response delete(self, web.Request request, str config_key)
Definition: view.py:144
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: calendar.py:44
CalendarEvent _get_calendar_event(Event event)
Definition: calendar.py:201
Event _parse_event(dict[str, Any] event)
Definition: calendar.py:175