hass-core/homeassistant/components/caldav/calendar.py
Allen Porter a95aa4e15f
Add config flow to CalDAV (#103215)
* Initial caldav config flow with broken calendar platform

* Set up calendar entities

* Remove separate caldav entity

* Update tests after merge

* Readbility improvements

* Address lint issues

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Add checking for duplicate configuration entries

* Use verify SSL as input into caldav and simplify test setup

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-11-02 19:48:56 -07:00

226 lines
7.5 KiB
Python

"""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 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 .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
OFFSET = "!!"
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,
}
)
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
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 = client.principal().calendars()
calendar_devices = []
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 = 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],
)
calendar_devices.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]:
if (
supported_components := calendar.get_supported_components()
) and "VEVENT" not in supported_components:
_LOGGER.debug(
"Ignoring calendar '%s' (components=%s)",
calendar.name,
supported_components,
)
continue
name = calendar.name
device_id = calendar.name
entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
coordinator = CalDavUpdateCoordinator(
hass,
calendar=calendar,
days=days,
include_all_day=False,
search=None,
)
calendar_devices.append(
WebDavCalendarEntity(name, entity_id, coordinator, supports_offset=True)
)
add_entities(calendar_devices, 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 hass.async_add_executor_job(client.principal().calendars)
async_add_entities(
(
WebDavCalendarEntity(
calendar.name,
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()