2019-04-03 15:40:03 +00:00
|
|
|
"""Support for WebDav Calendar."""
|
2018-01-21 06:35:38 +00:00
|
|
|
from datetime import datetime, timedelta
|
2017-12-10 16:44:28 +00:00
|
|
|
import logging
|
|
|
|
import re
|
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
|
|
|
|
from homeassistant.components.calendar import (
|
2018-06-15 15:16:31 +00:00
|
|
|
PLATFORM_SCHEMA, CalendarEventDevice, get_date)
|
2017-12-10 16:44:28 +00:00
|
|
|
from homeassistant.const import (
|
2019-04-27 15:40:20 +00:00
|
|
|
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL)
|
2018-01-21 06:35:38 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
from homeassistant.util import Throttle, dt
|
2017-12-10 16:44:28 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
CONF_DEVICE_ID = 'device_id'
|
|
|
|
CONF_CALENDARS = 'calendars'
|
|
|
|
CONF_CUSTOM_CALENDARS = 'custom_calendars'
|
|
|
|
CONF_CALENDAR = 'calendar'
|
|
|
|
CONF_SEARCH = 'search'
|
|
|
|
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
2017-12-17 19:53:40 +00:00
|
|
|
# pylint: disable=no-value-for-parameter
|
|
|
|
vol.Required(CONF_URL): vol.Url(),
|
2017-12-10 16:44:28 +00:00
|
|
|
vol.Optional(CONF_CALENDARS, default=[]):
|
|
|
|
vol.All(cv.ensure_list, vol.Schema([
|
|
|
|
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.Schema({
|
|
|
|
vol.Required(CONF_CALENDAR): cv.string,
|
2018-01-21 06:35:38 +00:00
|
|
|
vol.Required(CONF_NAME): cv.string,
|
|
|
|
vol.Required(CONF_SEARCH): cv.string,
|
2017-12-10 16:44:28 +00:00
|
|
|
})
|
2019-04-27 15:40:20 +00:00
|
|
|
])),
|
|
|
|
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean
|
2017-12-10 16:44:28 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
|
|
|
|
|
|
|
|
2018-08-24 14:37:30 +00:00
|
|
|
def setup_platform(hass, config, add_entities, disc_info=None):
|
2017-12-10 16:44:28 +00:00
|
|
|
"""Set up the WebDav Calendar platform."""
|
|
|
|
import caldav
|
|
|
|
|
2018-01-21 06:35:38 +00:00
|
|
|
url = config.get(CONF_URL)
|
|
|
|
username = config.get(CONF_USERNAME)
|
|
|
|
password = config.get(CONF_PASSWORD)
|
|
|
|
|
2019-04-27 15:40:20 +00:00
|
|
|
client = caldav.DAVClient(url, None, username, password,
|
|
|
|
ssl_verify_cert=config.get(CONF_VERIFY_SSL))
|
2017-12-10 16:44:28 +00:00
|
|
|
|
|
|
|
calendars = client.principal().calendars()
|
|
|
|
|
|
|
|
calendar_devices = []
|
|
|
|
for calendar in list(calendars):
|
|
|
|
# If a calendar name was given in the configuration,
|
|
|
|
# ignore all the others
|
|
|
|
if (config.get(CONF_CALENDARS)
|
|
|
|
and calendar.name not in config.get(CONF_CALENDARS)):
|
|
|
|
_LOGGER.debug("Ignoring calendar '%s'", calendar.name)
|
|
|
|
continue
|
|
|
|
|
2018-01-21 06:35:38 +00:00
|
|
|
# Create additional calendars based on custom filtering rules
|
2017-12-10 16:44:28 +00:00
|
|
|
for cust_calendar in config.get(CONF_CUSTOM_CALENDARS):
|
|
|
|
# Check that the base calendar matches
|
|
|
|
if cust_calendar.get(CONF_CALENDAR) != calendar.name:
|
|
|
|
continue
|
|
|
|
|
|
|
|
device_data = {
|
|
|
|
CONF_NAME: cust_calendar.get(CONF_NAME),
|
|
|
|
CONF_DEVICE_ID: "{} {}".format(
|
|
|
|
cust_calendar.get(CONF_CALENDAR),
|
|
|
|
cust_calendar.get(CONF_NAME)),
|
|
|
|
}
|
|
|
|
|
|
|
|
calendar_devices.append(
|
2018-01-21 06:35:38 +00:00
|
|
|
WebDavCalendarEventDevice(
|
|
|
|
hass, device_data, calendar, True,
|
|
|
|
cust_calendar.get(CONF_SEARCH)))
|
2017-12-10 16:44:28 +00:00
|
|
|
|
|
|
|
# Create a default calendar if there was no custom one
|
|
|
|
if not config.get(CONF_CUSTOM_CALENDARS):
|
|
|
|
device_data = {
|
|
|
|
CONF_NAME: calendar.name,
|
2018-06-15 15:16:31 +00:00
|
|
|
CONF_DEVICE_ID: calendar.name,
|
2017-12-10 16:44:28 +00:00
|
|
|
}
|
|
|
|
calendar_devices.append(
|
|
|
|
WebDavCalendarEventDevice(hass, device_data, calendar)
|
|
|
|
)
|
|
|
|
|
2018-08-24 14:37:30 +00:00
|
|
|
add_entities(calendar_devices)
|
2017-12-10 16:44:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
class WebDavCalendarEventDevice(CalendarEventDevice):
|
|
|
|
"""A device for getting the next Task from a WebDav Calendar."""
|
|
|
|
|
2018-01-21 06:35:38 +00:00
|
|
|
def __init__(self, hass, device_data, calendar, all_day=False,
|
2017-12-10 16:44:28 +00:00
|
|
|
search=None):
|
|
|
|
"""Create the WebDav Calendar Event Device."""
|
|
|
|
self.data = WebDavCalendarData(calendar, all_day, search)
|
|
|
|
super().__init__(hass, device_data)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return the device state attributes."""
|
|
|
|
if self.data.event is None:
|
|
|
|
# No tasks, we don't REALLY need to show anything.
|
|
|
|
return {}
|
|
|
|
|
|
|
|
attributes = super().device_state_attributes
|
|
|
|
return attributes
|
|
|
|
|
2018-06-15 15:16:31 +00:00
|
|
|
async def async_get_events(self, hass, start_date, end_date):
|
|
|
|
"""Get all events in a specific time frame."""
|
|
|
|
return await self.data.async_get_events(hass, start_date, end_date)
|
|
|
|
|
2017-12-10 16:44:28 +00:00
|
|
|
|
2018-07-20 08:45:20 +00:00
|
|
|
class WebDavCalendarData:
|
2017-12-10 16:44:28 +00:00
|
|
|
"""Class to utilize the calendar dav client object to get next event."""
|
|
|
|
|
|
|
|
def __init__(self, calendar, include_all_day, search):
|
|
|
|
"""Set up how we are going to search the WebDav calendar."""
|
|
|
|
self.calendar = calendar
|
|
|
|
self.include_all_day = include_all_day
|
|
|
|
self.search = search
|
|
|
|
self.event = None
|
|
|
|
|
2018-06-15 15:16:31 +00:00
|
|
|
async def async_get_events(self, hass, start_date, end_date):
|
|
|
|
"""Get all events in a specific time frame."""
|
|
|
|
# Get event list from the current calendar
|
|
|
|
vevent_list = await hass.async_add_job(self.calendar.date_search,
|
|
|
|
start_date, end_date)
|
|
|
|
event_list = []
|
|
|
|
for event in vevent_list:
|
|
|
|
vevent = event.instance.vevent
|
|
|
|
uid = None
|
|
|
|
if hasattr(vevent, 'uid'):
|
|
|
|
uid = vevent.uid.value
|
|
|
|
data = {
|
|
|
|
"uid": uid,
|
|
|
|
"title": vevent.summary.value,
|
|
|
|
"start": self.get_hass_date(vevent.dtstart.value),
|
|
|
|
"end": self.get_hass_date(self.get_end_date(vevent)),
|
|
|
|
"location": self.get_attr_value(vevent, "location"),
|
|
|
|
"description": self.get_attr_value(vevent, "description"),
|
|
|
|
}
|
|
|
|
|
|
|
|
data['start'] = get_date(data['start']).isoformat()
|
|
|
|
data['end'] = get_date(data['end']).isoformat()
|
|
|
|
|
|
|
|
event_list.append(data)
|
|
|
|
|
|
|
|
return event_list
|
|
|
|
|
2017-12-10 16:44:28 +00:00
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
|
|
def update(self):
|
|
|
|
"""Get the latest data."""
|
|
|
|
# We have to retrieve the results for the whole day as the server
|
|
|
|
# won't return events that have already started
|
|
|
|
results = self.calendar.date_search(
|
|
|
|
dt.start_of_local_day(),
|
|
|
|
dt.start_of_local_day() + timedelta(days=1)
|
|
|
|
)
|
|
|
|
|
|
|
|
# dtstart can be a date or datetime depending if the event lasts a
|
|
|
|
# whole day. Convert everything to datetime to be able to sort it
|
|
|
|
results.sort(key=lambda x: self.to_datetime(
|
|
|
|
x.instance.vevent.dtstart.value
|
|
|
|
))
|
|
|
|
|
|
|
|
vevent = next((
|
|
|
|
event.instance.vevent for event in results
|
|
|
|
if (self.is_matching(event.instance.vevent, self.search)
|
|
|
|
and (not self.is_all_day(event.instance.vevent)
|
|
|
|
or self.include_all_day)
|
|
|
|
and not self.is_over(event.instance.vevent))), None)
|
|
|
|
|
|
|
|
# If no matching event could be found
|
|
|
|
if vevent is None:
|
|
|
|
_LOGGER.debug(
|
|
|
|
"No matching event found in the %d results for %s",
|
2018-01-21 06:35:38 +00:00
|
|
|
len(results), self.calendar.name)
|
2017-12-10 16:44:28 +00:00
|
|
|
self.event = None
|
|
|
|
return True
|
|
|
|
|
|
|
|
# Populate the entity attributes with the event values
|
|
|
|
self.event = {
|
|
|
|
"summary": vevent.summary.value,
|
|
|
|
"start": self.get_hass_date(vevent.dtstart.value),
|
2018-02-20 23:37:34 +00:00
|
|
|
"end": self.get_hass_date(self.get_end_date(vevent)),
|
2017-12-10 16:44:28 +00:00
|
|
|
"location": self.get_attr_value(vevent, "location"),
|
|
|
|
"description": self.get_attr_value(vevent, "description")
|
|
|
|
}
|
|
|
|
return True
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def is_matching(vevent, search):
|
2018-01-27 19:58:27 +00:00
|
|
|
"""Return if the event matches the filter criteria."""
|
2017-12-10 16:44:28 +00:00
|
|
|
if search is None:
|
|
|
|
return True
|
|
|
|
|
|
|
|
pattern = re.compile(search)
|
|
|
|
return (hasattr(vevent, "summary")
|
|
|
|
and pattern.match(vevent.summary.value)
|
|
|
|
or hasattr(vevent, "location")
|
|
|
|
and pattern.match(vevent.location.value)
|
|
|
|
or hasattr(vevent, "description")
|
|
|
|
and pattern.match(vevent.description.value))
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def is_all_day(vevent):
|
|
|
|
"""Return if the event last the whole day."""
|
|
|
|
return not isinstance(vevent.dtstart.value, datetime)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def is_over(vevent):
|
|
|
|
"""Return if the event is over."""
|
2018-03-15 17:58:11 +00:00
|
|
|
return dt.now() >= WebDavCalendarData.to_datetime(
|
|
|
|
WebDavCalendarData.get_end_date(vevent)
|
|
|
|
)
|
2017-12-10 16:44:28 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_hass_date(obj):
|
|
|
|
"""Return if the event matches."""
|
|
|
|
if isinstance(obj, datetime):
|
|
|
|
return {"dateTime": obj.isoformat()}
|
|
|
|
|
|
|
|
return {"date": obj.isoformat()}
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def to_datetime(obj):
|
|
|
|
"""Return a datetime."""
|
|
|
|
if isinstance(obj, datetime):
|
|
|
|
return obj
|
|
|
|
return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min))
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_attr_value(obj, attribute):
|
|
|
|
"""Return the value of the attribute if defined."""
|
|
|
|
if hasattr(obj, attribute):
|
|
|
|
return getattr(obj, attribute).value
|
|
|
|
return None
|
2018-02-20 23:37:34 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_end_date(obj):
|
|
|
|
"""Return the end datetime as determined by dtend or duration."""
|
|
|
|
if hasattr(obj, "dtend"):
|
|
|
|
enddate = obj.dtend.value
|
|
|
|
|
|
|
|
elif hasattr(obj, "duration"):
|
|
|
|
enddate = obj.dtstart.value + obj.duration.value
|
|
|
|
|
|
|
|
else:
|
|
|
|
enddate = obj.dtstart.value + timedelta(days=1)
|
|
|
|
|
2018-03-15 17:58:11 +00:00
|
|
|
return enddate
|