"""Calendar platform for a Local Calendar.""" from __future__ import annotations from datetime import datetime import logging from typing import Any from ical.calendar import Calendar from ical.calendar_stream import IcsCalendarStream from ical.event import Event from ical.store import EventStore from ical.types import Range, Recur from pydantic import ValidationError import voluptuous as vol from homeassistant.components.calendar import ( EVENT_DESCRIPTION, EVENT_END, EVENT_RRULE, EVENT_START, EVENT_SUMMARY, CalendarEntity, CalendarEntityFeature, CalendarEvent, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from .const import CONF_CALENDAR_NAME, DOMAIN from .store import LocalCalendarStore _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the local calendar platform.""" store = hass.data[DOMAIN][config_entry.entry_id] ics = await store.async_load() calendar = IcsCalendarStream.calendar_from_ics(ics) name = config_entry.data[CONF_CALENDAR_NAME] entity = LocalCalendarEntity(store, calendar, name, unique_id=config_entry.entry_id) async_add_entities([entity], True) class LocalCalendarEntity(CalendarEntity): """A calendar entity backed by a local iCalendar file.""" _attr_has_entity_name = True _attr_supported_features = ( CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT ) def __init__( self, store: LocalCalendarStore, calendar: Calendar, name: str, unique_id: str, ) -> None: """Initialize LocalCalendarEntity.""" self._store = store self._calendar = calendar self._event: CalendarEvent | None = None self._attr_name = name.capitalize() self._attr_unique_id = unique_id @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" return self._event async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" events = self._calendar.timeline_tz(dt_util.DEFAULT_TIME_ZONE).overlapping( dt_util.as_local(start_date), dt_util.as_local(end_date), ) return [_get_calendar_event(event) for event in events] async def async_update(self) -> None: """Update entity state with the next upcoming event.""" events = self._calendar.timeline_tz(dt_util.DEFAULT_TIME_ZONE).active_after( dt_util.now() ) if event := next(events, None): self._event = _get_calendar_event(event) else: self._event = None async def _async_store(self) -> None: """Persist the calendar to disk.""" content = IcsCalendarStream.calendar_to_ics(self._calendar) await self._store.async_store(content) async def async_create_event(self, **kwargs: Any) -> None: """Add a new event to calendar.""" event_data = { EVENT_SUMMARY: kwargs[EVENT_SUMMARY], EVENT_START: kwargs[EVENT_START], EVENT_END: kwargs[EVENT_END], EVENT_DESCRIPTION: kwargs.get(EVENT_DESCRIPTION), } try: event = Event.parse_obj(event_data) except ValidationError as err: _LOGGER.debug( "Error parsing event input fields: %s (%s)", event_data, str(err) ) raise vol.Invalid("Error parsing event input fields") from err if rrule := kwargs.get(EVENT_RRULE): event.rrule = Recur.from_rrule(rrule) EventStore(self._calendar).add(event) await self._async_store() await self.async_update_ha_state(force_refresh=True) async def async_delete_event( self, uid: str, recurrence_id: str | None = None, recurrence_range: str | None = None, ) -> None: """Delete an event on the calendar.""" range_value: Range = Range.NONE if recurrence_range == Range.THIS_AND_FUTURE: range_value = Range.THIS_AND_FUTURE EventStore(self._calendar).delete( uid, recurrence_id=recurrence_id, recurrence_range=range_value, ) await self._async_store() await self.async_update_ha_state(force_refresh=True) def _get_calendar_event(event: Event) -> CalendarEvent: """Return a CalendarEvent from an API event.""" return CalendarEvent( summary=event.summary, start=event.start, end=event.end, description=event.description, uid=event.uid, rrule=event.rrule.as_rrule_str() if event.rrule else None, recurrence_id=event.recurrence_id, )