"""Support for WebDav Calendar.""" from __future__ import annotations from datetime import datetime import logging import caldav import voluptuous as vol from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, CalendarEntity, CalendarEvent, is_offset_reached, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .api import async_get_calendars from .const import DOMAIN from .coordinator import CalDavUpdateCoordinator _LOGGER = logging.getLogger(__name__) CONF_CALENDARS = "calendars" CONF_CUSTOM_CALENDARS = "custom_calendars" CONF_CALENDAR = "calendar" CONF_SEARCH = "search" CONF_DAYS = "days" # Number of days to look ahead for next event when configured by ConfigEntry CONFIG_ENTRY_DEFAULT_DAYS = 7 # Only allow VCALENDARs that support this component type SUPPORTED_COMPONENT = "VEVENT" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): vol.Url(), vol.Optional(CONF_CALENDARS, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, vol.Optional(CONF_CUSTOM_CALENDARS, default=[]): vol.All( cv.ensure_list, [ vol.Schema( { vol.Required(CONF_CALENDAR): cv.string, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_SEARCH): cv.string, } ) ], ), vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Optional(CONF_DAYS, default=1): cv.positive_int, } ) async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, disc_info: DiscoveryInfoType | None = None, ) -> None: """Set up the WebDav Calendar platform.""" url = config[CONF_URL] username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) days = config[CONF_DAYS] client = caldav.DAVClient( url, None, username, password, ssl_verify_cert=config[CONF_VERIFY_SSL] ) calendars = await async_get_calendars(hass, client, SUPPORTED_COMPONENT) entities = [] device_id: str | None for calendar in list(calendars): # If a calendar name was given in the configuration, # ignore all the others if config[CONF_CALENDARS] and calendar.name not in config[CONF_CALENDARS]: _LOGGER.debug("Ignoring calendar '%s'", calendar.name) continue # Create additional calendars based on custom filtering rules for cust_calendar in config[CONF_CUSTOM_CALENDARS]: # Check that the base calendar matches if cust_calendar[CONF_CALENDAR] != calendar.name: continue name = cust_calendar[CONF_NAME] device_id = f"{cust_calendar[CONF_CALENDAR]} {cust_calendar[CONF_NAME]}" entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass) coordinator = CalDavUpdateCoordinator( hass, calendar=calendar, days=days, include_all_day=True, search=cust_calendar[CONF_SEARCH], ) entities.append( WebDavCalendarEntity(name, entity_id, coordinator, supports_offset=True) ) # Create a default calendar if there was no custom one for all calendars # that support events. if not config[CONF_CUSTOM_CALENDARS]: name = calendar.name device_id = calendar.name entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass) coordinator = CalDavUpdateCoordinator( hass, calendar=calendar, days=days, include_all_day=False, search=None, ) entities.append( WebDavCalendarEntity(name, entity_id, coordinator, supports_offset=True) ) async_add_entities(entities, True) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the CalDav calendar platform for a config entry.""" client: caldav.DAVClient = hass.data[DOMAIN][entry.entry_id] calendars = await async_get_calendars(hass, client, SUPPORTED_COMPONENT) async_add_entities( ( WebDavCalendarEntity( calendar.name, async_generate_entity_id(ENTITY_ID_FORMAT, calendar.name, hass=hass), CalDavUpdateCoordinator( hass, calendar=calendar, days=CONFIG_ENTRY_DEFAULT_DAYS, include_all_day=True, search=None, ), unique_id=f"{entry.entry_id}-{calendar.id}", ) for calendar in calendars if calendar.name ), True, ) class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarEntity): """A device for getting the next Task from a WebDav Calendar.""" def __init__( self, name: str, entity_id: str, coordinator: CalDavUpdateCoordinator, unique_id: str | None = None, supports_offset: bool = False, ) -> None: """Create the WebDav Calendar Event Device.""" super().__init__(coordinator) self.entity_id = entity_id self._event: CalendarEvent | None = None self._attr_name = name if unique_id is not None: self._attr_unique_id = unique_id self._supports_offset = supports_offset @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.""" return await self.coordinator.async_get_events(hass, start_date, end_date) @callback def _handle_coordinator_update(self) -> None: """Update event data.""" self._event = self.coordinator.data if self._supports_offset: self._attr_extra_state_attributes = { "offset_reached": is_offset_reached( self._event.start_datetime_local, self.coordinator.offset ) if self._event else False } super()._handle_coordinator_update() async def async_added_to_hass(self) -> None: """When entity is added to hass update state from existing coordinator data.""" await super().async_added_to_hass() self._handle_coordinator_update()