homeassistant-config/custom_components/ics_calendar/calendar.py

292 lines
9.5 KiB
Python

"""Support for ICS Calendar."""
import logging
from datetime import datetime, timedelta
from typing import Optional
# import homeassistant.helpers.config_validation as cv
# import voluptuous as vol
from homeassistant.components.calendar import (
ENTITY_ID_FORMAT,
CalendarEntity,
CalendarEvent,
extract_offset,
is_offset_reached,
)
from homeassistant.const import (
CONF_EXCLUDE,
CONF_INCLUDE,
CONF_NAME,
CONF_PASSWORD,
CONF_PREFIX,
CONF_URL,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import Throttle
from homeassistant.util.dt import now as hanow
from . import (
CONF_ACCEPT_HEADER,
CONF_CALENDARS,
CONF_DAYS,
CONF_DOWNLOAD_INTERVAL,
CONF_INCLUDE_ALL_DAY,
CONF_OFFSET_HOURS,
CONF_PARSER,
CONF_USER_AGENT,
)
from .calendardata import CalendarData
from .filter import Filter
from .icalendarparser import ICalendarParser
_LOGGER = logging.getLogger(__name__)
OFFSET = "!!"
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
):
"""Set up ics_calendar platform.
:param hass: Home Assistant object
:type hass: HomeAssistant
:param config: Config information for the platform
:type config: ConfigType
:param add_entities: Callback to add entities to HA
:type add_entities: AddEntitiesCallback
:param discovery_info: Config information for the platform
:type discovery_info: DiscoveryInfoType | None, optional
"""
_LOGGER.debug("Setting up ics calendars")
if discovery_info is not None:
calendars: list = discovery_info.get(CONF_CALENDARS)
else:
calendars: list = config.get(CONF_CALENDARS)
calendar_devices = []
for calendar in calendars:
device_data = {
CONF_NAME: calendar.get(CONF_NAME),
CONF_URL: calendar.get(CONF_URL),
CONF_INCLUDE_ALL_DAY: calendar.get(CONF_INCLUDE_ALL_DAY),
CONF_USERNAME: calendar.get(CONF_USERNAME),
CONF_PASSWORD: calendar.get(CONF_PASSWORD),
CONF_PARSER: calendar.get(CONF_PARSER),
CONF_PREFIX: calendar.get(CONF_PREFIX),
CONF_DAYS: calendar.get(CONF_DAYS),
CONF_DOWNLOAD_INTERVAL: calendar.get(CONF_DOWNLOAD_INTERVAL),
CONF_USER_AGENT: calendar.get(CONF_USER_AGENT),
CONF_EXCLUDE: calendar.get(CONF_EXCLUDE),
CONF_INCLUDE: calendar.get(CONF_INCLUDE),
CONF_OFFSET_HOURS: calendar.get(CONF_OFFSET_HOURS),
CONF_ACCEPT_HEADER: calendar.get(CONF_ACCEPT_HEADER),
}
device_id = f"{device_data[CONF_NAME]}"
entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
calendar_devices.append(ICSCalendarEntity(entity_id, device_data))
add_entities(calendar_devices)
class ICSCalendarEntity(CalendarEntity):
"""A CalendarEntity for an ICS Calendar."""
def __init__(self, entity_id: str, device_data):
"""Construct ICSCalendarEntity.
:param entity_id: Entity id for the calendar
:type entity_id: str
:param device_data: dict describing the calendar
:type device_data: dict
"""
_LOGGER.debug(
"Initializing calendar: %s with URL: %s",
device_data[CONF_NAME],
device_data[CONF_URL],
)
self.data = ICSCalendarData(device_data)
self.entity_id = entity_id
self._event = None
self._name = device_data[CONF_NAME]
self._last_call = None
@property
def event(self) -> Optional[CalendarEvent]:
"""Return the current or next upcoming event or None.
:return: The current event as a dict
:rtype: dict
"""
return self._event
@property
def name(self):
"""Return the name of the calendar."""
return self._name
@property
def should_poll(self):
"""Indicate if the calendar should be polled.
If the last call to update or get_api_events was not within the minimum
update time, then async_schedule_update_ha_state(True) is also called.
:return: True
:rtype: boolean
"""
this_call = hanow()
if (
self._last_call is None
or (this_call - self._last_call) > MIN_TIME_BETWEEN_UPDATES
):
self._last_call = this_call
self.async_schedule_update_ha_state(True)
return True
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
"""Get all events in a specific time frame.
:param hass: Home Assistant object
:type hass: HomeAssistant
:param start_date: The first starting date to consider
:type start_date: datetime
:param end_date: The last starting date to consider
:type end_date: datetime
"""
_LOGGER.debug(
"%s: async_get_events called; calling internal.", self.name
)
return await self.data.async_get_events(hass, start_date, end_date)
def update(self):
"""Get the current or next event."""
self.data.update()
self._event = self.data.event
self._attr_extra_state_attributes = {
"offset_reached": is_offset_reached(
self._event.start_datetime_local, self.data.offset
)
if self._event
else False
}
class ICSCalendarData: # pylint: disable=R0902
"""Class to use the calendar ICS client object to get next event."""
def __init__(self, device_data):
"""Set up how we are going to connect to the URL.
:param device_data Information about the calendar
"""
self.name = device_data[CONF_NAME]
self._days = device_data[CONF_DAYS]
self._offset_hours = device_data[CONF_OFFSET_HOURS]
self.include_all_day = device_data[CONF_INCLUDE_ALL_DAY]
self._summary_prefix: str = device_data[CONF_PREFIX]
self.parser = ICalendarParser.get_instance(device_data[CONF_PARSER])
self.parser.set_filter(
Filter(device_data[CONF_EXCLUDE], device_data[CONF_INCLUDE])
)
self.offset = None
self.event = None
self._calendar_data = CalendarData(
_LOGGER,
self.name,
device_data[CONF_URL],
timedelta(minutes=device_data[CONF_DOWNLOAD_INTERVAL]),
)
self._calendar_data.set_headers(
device_data[CONF_USERNAME],
device_data[CONF_PASSWORD],
device_data[CONF_USER_AGENT],
device_data[CONF_ACCEPT_HEADER],
)
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
"""Get all events in a specific time frame.
:param hass: Home Assistant object
:type hass: HomeAssistant
:param start_date: The first starting date to consider
:type start_date: datetime
:param end_date: The last starting date to consider
:type end_date: datetime
"""
event_list = []
if await hass.async_add_executor_job(
self._calendar_data.download_calendar
):
_LOGGER.debug("%s: Setting calendar content", self.name)
self.parser.set_content(self._calendar_data.get())
try:
event_list = self.parser.get_event_list(
start=start_date,
end=end_date,
include_all_day=self.include_all_day,
offset_hours=self._offset_hours,
)
except: # pylint: disable=W0702
_LOGGER.error(
"async_get_events: %s: Failed to parse ICS!",
self.name,
exc_info=True,
)
event_list = []
for event in event_list:
event.summary = self._summary_prefix + event.summary
return event_list
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the current or next event."""
_LOGGER.debug("%s: Update was called", self.name)
if self._calendar_data.download_calendar():
_LOGGER.debug("%s: Setting calendar content", self.name)
self.parser.set_content(self._calendar_data.get())
try:
self.event = self.parser.get_current_event(
include_all_day=self.include_all_day,
now=hanow(),
days=self._days,
offset_hours=self._offset_hours,
)
except: # pylint: disable=W0702
_LOGGER.error(
"update: %s: Failed to parse ICS!", self.name, exc_info=True
)
if self.event is not None:
_LOGGER.debug(
"%s: got event: %s; start: %s; end: %s; all_day: %s",
self.name,
self.event.summary,
self.event.start,
self.event.end,
self.event.all_day,
)
(summary, offset) = extract_offset(self.event.summary, OFFSET)
self.event.summary = self._summary_prefix + summary
self.offset = offset
return True
_LOGGER.debug("%s: No event found!", self.name)
return False