Rewrite calendar component (#24950)
* Correct google calendar test name * Rewrite calendar component * Save component in hass.data. * Rename device_state_attributes to state_attributes. * Remove offset attribute from base state_attributes. * Extract offset helpers to calendar component. * Clean imports. * Remove stale constants. * Remove name and add async_get_events. * Add normalize_event helper function. Copied from #21495. * Add event property to base entity. * Use event property for calendar state. * Ensure event start and end. * Remove entity init. * Add comment about event data class. * Temporary keep old start and end datetime format. * Convert demo calendar * Convert google calendar * Convert google calendar. * Clean up google component. * Keep offset feature by using offset helpers. * Convert caldav calendar * Clean up caldav calendar. * Update caldav cal on addition. * Bring back offset to caldav calendar. * Copy caldav event on update. * Convert todoist calendarpull/25076/head
parent
c6af8811fb
commit
177f5a35ae
|
@ -1,4 +1,5 @@
|
|||
"""Support for WebDav Calendar."""
|
||||
import copy
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import re
|
||||
|
@ -6,37 +7,38 @@ import re
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.calendar import (
|
||||
PLATFORM_SCHEMA, CalendarEventDevice, get_date)
|
||||
ENTITY_ID_FORMAT, PLATFORM_SCHEMA, CalendarEventDevice, calculate_offset,
|
||||
get_date, is_offset_reached)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
CONF_CALENDARS = 'calendars'
|
||||
CONF_CUSTOM_CALENDARS = 'custom_calendars'
|
||||
CONF_CALENDAR = 'calendar'
|
||||
CONF_SEARCH = 'search'
|
||||
|
||||
OFFSET = '!!'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=no-value-for-parameter
|
||||
vol.Required(CONF_URL): vol.Url(),
|
||||
vol.Optional(CONF_CALENDARS, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Schema([
|
||||
cv.string
|
||||
])),
|
||||
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.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
|
||||
})
|
||||
|
||||
|
@ -47,12 +49,12 @@ def setup_platform(hass, config, add_entities, disc_info=None):
|
|||
"""Set up the WebDav Calendar platform."""
|
||||
import caldav
|
||||
|
||||
url = config.get(CONF_URL)
|
||||
url = config[CONF_URL]
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
client = caldav.DAVClient(url, None, username, password,
|
||||
ssl_verify_cert=config.get(CONF_VERIFY_SSL))
|
||||
client = caldav.DAVClient(
|
||||
url, None, username, password, ssl_verify_cert=config[CONF_VERIFY_SSL])
|
||||
|
||||
calendars = client.principal().calendars()
|
||||
|
||||
|
@ -60,65 +62,83 @@ def setup_platform(hass, config, add_entities, disc_info=None):
|
|||
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)):
|
||||
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.get(CONF_CUSTOM_CALENDARS):
|
||||
for cust_calendar in config[CONF_CUSTOM_CALENDARS]:
|
||||
# Check that the base calendar matches
|
||||
if cust_calendar.get(CONF_CALENDAR) != calendar.name:
|
||||
if cust_calendar[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)),
|
||||
}
|
||||
|
||||
name = cust_calendar[CONF_NAME]
|
||||
device_id = "{} {}".format(
|
||||
cust_calendar[CONF_CALENDAR], cust_calendar[CONF_NAME])
|
||||
entity_id = generate_entity_id(
|
||||
ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||
calendar_devices.append(
|
||||
WebDavCalendarEventDevice(
|
||||
hass, device_data, calendar, True,
|
||||
cust_calendar.get(CONF_SEARCH)))
|
||||
name, calendar, entity_id, True,
|
||||
cust_calendar[CONF_SEARCH]))
|
||||
|
||||
# Create a default calendar if there was no custom one
|
||||
if not config.get(CONF_CUSTOM_CALENDARS):
|
||||
device_data = {
|
||||
CONF_NAME: calendar.name,
|
||||
CONF_DEVICE_ID: calendar.name,
|
||||
}
|
||||
if not config[CONF_CUSTOM_CALENDARS]:
|
||||
name = calendar.name
|
||||
device_id = calendar.name
|
||||
entity_id = generate_entity_id(
|
||||
ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||
calendar_devices.append(
|
||||
WebDavCalendarEventDevice(hass, device_data, calendar)
|
||||
WebDavCalendarEventDevice(name, calendar, entity_id)
|
||||
)
|
||||
|
||||
add_entities(calendar_devices)
|
||||
add_entities(calendar_devices, True)
|
||||
|
||||
|
||||
class WebDavCalendarEventDevice(CalendarEventDevice):
|
||||
"""A device for getting the next Task from a WebDav Calendar."""
|
||||
|
||||
def __init__(self, hass, device_data, calendar, all_day=False,
|
||||
search=None):
|
||||
def __init__(self, name, calendar, entity_id, all_day=False, search=None):
|
||||
"""Create the WebDav Calendar Event Device."""
|
||||
self.data = WebDavCalendarData(calendar, all_day, search)
|
||||
super().__init__(hass, device_data)
|
||||
self.entity_id = entity_id
|
||||
self._event = None
|
||||
self._name = name
|
||||
self._offset_reached = False
|
||||
|
||||
@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 {}
|
||||
return {
|
||||
'offset_reached': self._offset_reached,
|
||||
}
|
||||
|
||||
attributes = super().device_state_attributes
|
||||
return attributes
|
||||
@property
|
||||
def event(self):
|
||||
"""Return the next upcoming event."""
|
||||
return self._event
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
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)
|
||||
|
||||
def update(self):
|
||||
"""Update event data."""
|
||||
self.data.update()
|
||||
event = copy.deepcopy(self.data.event)
|
||||
if event is None:
|
||||
self._event = event
|
||||
return
|
||||
event = calculate_offset(event, OFFSET)
|
||||
self._offset_reached = is_offset_reached(event)
|
||||
self._event = event
|
||||
|
||||
|
||||
class WebDavCalendarData:
|
||||
"""Class to utilize the calendar dav client object to get next event."""
|
||||
|
@ -174,9 +194,11 @@ class WebDavCalendarData:
|
|||
))
|
||||
|
||||
vevent = next((
|
||||
event.instance.vevent for event in results
|
||||
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)
|
||||
and (
|
||||
not self.is_all_day(event.instance.vevent)
|
||||
or self.include_all_day)
|
||||
and not self.is_over(event.instance.vevent))), None)
|
||||
|
||||
|
@ -186,7 +208,7 @@ class WebDavCalendarData:
|
|||
"No matching event found in the %d results for %s",
|
||||
len(results), self.calendar.name)
|
||||
self.event = None
|
||||
return True
|
||||
return
|
||||
|
||||
# Populate the entity attributes with the event values
|
||||
self.event = {
|
||||
|
@ -196,7 +218,6 @@ class WebDavCalendarData:
|
|||
"location": self.get_attr_value(vevent, "location"),
|
||||
"description": self.get_attr_value(vevent, "description")
|
||||
}
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_matching(vevent, search):
|
||||
|
@ -205,7 +226,8 @@ class WebDavCalendarData:
|
|||
return True
|
||||
|
||||
pattern = re.compile(search)
|
||||
return (hasattr(vevent, "summary")
|
||||
return (
|
||||
hasattr(vevent, "summary")
|
||||
and pattern.match(vevent.summary.value)
|
||||
or hasattr(vevent, "location")
|
||||
and pattern.match(vevent.location.value)
|
||||
|
|
|
@ -1,35 +1,29 @@
|
|||
"""Support for Google Calendar event device sensors."""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import re
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.components.google import (
|
||||
CONF_OFFSET, CONF_DEVICE_ID, CONF_NAME)
|
||||
from homeassistant.components import http
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.helpers.config_validation import ( # noqa
|
||||
PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE)
|
||||
from homeassistant.helpers.config_validation import time_period_str
|
||||
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||
PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, time_period_str)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
from homeassistant.util import dt
|
||||
from homeassistant.components import http
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'calendar'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Track states and offer events for calendars."""
|
||||
component = EntityComponent(
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
|
||||
|
||||
hass.http.register_view(CalendarListView(component))
|
||||
|
@ -43,10 +37,6 @@ async def async_setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
DEFAULT_CONF_TRACK_NEW = True
|
||||
DEFAULT_CONF_OFFSET = '!!'
|
||||
|
||||
|
||||
def get_date(date):
|
||||
"""Get the dateTime from date or dateTime as a local."""
|
||||
if 'date' in date:
|
||||
|
@ -55,114 +45,41 @@ def get_date(date):
|
|||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
|
||||
|
||||
class CalendarEventDevice(Entity):
|
||||
"""A calendar event device."""
|
||||
def normalize_event(event):
|
||||
"""Normalize a calendar event."""
|
||||
normalized_event = {}
|
||||
|
||||
# Classes overloading this must set data to an object
|
||||
# with an update() method
|
||||
data = None
|
||||
start = event.get('start')
|
||||
end = event.get('end')
|
||||
start = get_date(start) if start is not None else None
|
||||
end = get_date(end) if end is not None else None
|
||||
normalized_event['dt_start'] = start
|
||||
normalized_event['dt_end'] = end
|
||||
|
||||
def __init__(self, hass, data):
|
||||
"""Create the Calendar Event Device."""
|
||||
self._name = data.get(CONF_NAME)
|
||||
self.dev_id = data.get(CONF_DEVICE_ID)
|
||||
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
||||
self.entity_id = generate_entity_id(
|
||||
ENTITY_ID_FORMAT, self.dev_id, hass=hass)
|
||||
|
||||
self._cal_data = {
|
||||
'all_day': False,
|
||||
'offset_time': dt.dt.timedelta(),
|
||||
'message': '',
|
||||
'start': None,
|
||||
'end': None,
|
||||
'location': '',
|
||||
'description': '',
|
||||
}
|
||||
|
||||
self.update()
|
||||
|
||||
def offset_reached(self):
|
||||
"""Have we reached the offset time specified in the event title."""
|
||||
if self._cal_data['start'] is None or \
|
||||
self._cal_data['offset_time'] == dt.dt.timedelta():
|
||||
return False
|
||||
|
||||
return self._cal_data['start'] + self._cal_data['offset_time'] <= \
|
||||
dt.now(self._cal_data['start'].tzinfo)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
start = self._cal_data.get('start', None)
|
||||
end = self._cal_data.get('end', None)
|
||||
start = start.strftime(DATE_STR_FORMAT) if start is not None else None
|
||||
end = end.strftime(DATE_STR_FORMAT) if end is not None else None
|
||||
normalized_event['start'] = start
|
||||
normalized_event['end'] = end
|
||||
|
||||
return {
|
||||
'message': self._cal_data.get('message', ''),
|
||||
'all_day': self._cal_data.get('all_day', False),
|
||||
'offset_reached': self.offset_reached(),
|
||||
'start_time': start,
|
||||
'end_time': end,
|
||||
'location': self._cal_data.get('location', None),
|
||||
'description': self._cal_data.get('description', None),
|
||||
}
|
||||
# cleanup the string so we don't have a bunch of double+ spaces
|
||||
summary = event.get('summary', '')
|
||||
normalized_event['message'] = re.sub(' +', '', summary).strip()
|
||||
normalized_event['location'] = event.get('location', '')
|
||||
normalized_event['description'] = event.get('description', '')
|
||||
normalized_event['all_day'] = 'date' in event['start']
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the calendar event."""
|
||||
start = self._cal_data.get('start', None)
|
||||
end = self._cal_data.get('end', None)
|
||||
if start is None or end is None:
|
||||
return STATE_OFF
|
||||
return normalized_event
|
||||
|
||||
now = dt.now()
|
||||
|
||||
if start <= now < end:
|
||||
return STATE_ON
|
||||
|
||||
if now >= end:
|
||||
self.cleanup()
|
||||
|
||||
return STATE_OFF
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup any start/end listeners that were setup."""
|
||||
self._cal_data = {
|
||||
'all_day': False,
|
||||
'offset_time': 0,
|
||||
'message': '',
|
||||
'start': None,
|
||||
'end': None,
|
||||
'location': None,
|
||||
'description': None
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Search for the next event."""
|
||||
if not self.data or not self.data.update():
|
||||
# update cached, don't do anything
|
||||
return
|
||||
|
||||
if not self.data.event:
|
||||
# we have no event to work on, make sure we're clean
|
||||
self.cleanup()
|
||||
return
|
||||
|
||||
start = get_date(self.data.event['start'])
|
||||
end = get_date(self.data.event['end'])
|
||||
|
||||
summary = self.data.event.get('summary', '')
|
||||
def calculate_offset(event, offset):
|
||||
"""Calculate event offset.
|
||||
|
||||
Return the updated event with the offset_time included.
|
||||
"""
|
||||
summary = event.get('summary', '')
|
||||
# check if we have an offset tag in the message
|
||||
# time is HH:MM or MM
|
||||
reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(self._offset)
|
||||
reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(offset)
|
||||
search = re.search(reg, summary)
|
||||
if search and search.group(1):
|
||||
time = search.group(1)
|
||||
|
@ -173,19 +90,74 @@ class CalendarEventDevice(Entity):
|
|||
time = '0:{}'.format(time)
|
||||
|
||||
offset_time = time_period_str(time)
|
||||
summary = (summary[:search.start()] + summary[search.end():]) \
|
||||
.strip()
|
||||
summary = (
|
||||
summary[:search.start()] + summary[search.end():]).strip()
|
||||
event['summary'] = summary
|
||||
else:
|
||||
offset_time = dt.dt.timedelta() # default it
|
||||
|
||||
# cleanup the string so we don't have a bunch of double+ spaces
|
||||
self._cal_data['message'] = re.sub(' +', '', summary).strip()
|
||||
self._cal_data['offset_time'] = offset_time
|
||||
self._cal_data['location'] = self.data.event.get('location', '')
|
||||
self._cal_data['description'] = self.data.event.get('description', '')
|
||||
self._cal_data['start'] = start
|
||||
self._cal_data['end'] = end
|
||||
self._cal_data['all_day'] = 'date' in self.data.event['start']
|
||||
event['offset_time'] = offset_time
|
||||
return event
|
||||
|
||||
|
||||
def is_offset_reached(event):
|
||||
"""Have we reached the offset time specified in the event title."""
|
||||
start = get_date(event['start'])
|
||||
if start is None or event['offset_time'] == dt.dt.timedelta():
|
||||
return False
|
||||
|
||||
return start + event['offset_time'] <= dt.now(start.tzinfo)
|
||||
|
||||
|
||||
class CalendarEventDevice(Entity):
|
||||
"""A calendar event device."""
|
||||
|
||||
@property
|
||||
def event(self):
|
||||
"""Return the next upcoming event."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the entity state attributes."""
|
||||
event = self.event
|
||||
if event is None:
|
||||
return None
|
||||
|
||||
event = normalize_event(event)
|
||||
return {
|
||||
'message': event['message'],
|
||||
'all_day': event['all_day'],
|
||||
'start_time': event['start'],
|
||||
'end_time': event['end'],
|
||||
'location': event['location'],
|
||||
'description': event['description'],
|
||||
}
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the calendar event."""
|
||||
event = self.event
|
||||
if event is None:
|
||||
return STATE_OFF
|
||||
|
||||
event = normalize_event(event)
|
||||
start = event['dt_start']
|
||||
end = event['dt_end']
|
||||
|
||||
if start is None or end is None:
|
||||
return STATE_OFF
|
||||
|
||||
now = dt.now()
|
||||
|
||||
if start <= now < end:
|
||||
return STATE_ON
|
||||
|
||||
return STATE_OFF
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Return calendar events within a datetime range."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CalendarEventView(http.HomeAssistantView):
|
||||
|
@ -227,11 +199,11 @@ class CalendarListView(http.HomeAssistantView):
|
|||
|
||||
async def get(self, request):
|
||||
"""Retrieve calendar list."""
|
||||
get_state = request.app['hass'].states.get
|
||||
hass = request.app['hass']
|
||||
calendar_list = []
|
||||
|
||||
for entity in self.component.entities:
|
||||
state = get_state(entity.entity_id)
|
||||
state = hass.states.get(entity.entity_id)
|
||||
calendar_list.append({
|
||||
"name": state.name,
|
||||
"entity_id": entity.entity_id,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""Demo platform that has two fake binary sensors."""
|
||||
import copy
|
||||
|
||||
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.components.calendar import CalendarEventDevice, get_date
|
||||
|
@ -12,27 +11,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
calendar_data_future = DemoGoogleCalendarDataFuture()
|
||||
calendar_data_current = DemoGoogleCalendarDataCurrent()
|
||||
add_entities([
|
||||
DemoGoogleCalendar(hass, calendar_data_future, {
|
||||
CONF_NAME: 'Calendar 1',
|
||||
CONF_DEVICE_ID: 'calendar_1',
|
||||
}),
|
||||
|
||||
DemoGoogleCalendar(hass, calendar_data_current, {
|
||||
CONF_NAME: 'Calendar 2',
|
||||
CONF_DEVICE_ID: 'calendar_2',
|
||||
}),
|
||||
DemoGoogleCalendar(hass, calendar_data_future, 'Calendar 1'),
|
||||
DemoGoogleCalendar(hass, calendar_data_current, 'Calendar 2'),
|
||||
])
|
||||
|
||||
|
||||
class DemoGoogleCalendarData:
|
||||
"""Representation of a Demo Calendar element."""
|
||||
|
||||
event = {}
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def update(self):
|
||||
"""Return true so entity knows we have new data."""
|
||||
return True
|
||||
event = None
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Get all events in a specific time frame."""
|
||||
|
@ -84,11 +71,21 @@ class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
|
|||
class DemoGoogleCalendar(CalendarEventDevice):
|
||||
"""Representation of a Demo Calendar element."""
|
||||
|
||||
def __init__(self, hass, calendar_data, data):
|
||||
"""Initialize Google Calendar but without the API calls."""
|
||||
def __init__(self, hass, calendar_data, name):
|
||||
"""Initialize demo calendar."""
|
||||
self.data = calendar_data
|
||||
super().__init__(hass, data)
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def event(self):
|
||||
"""Return the next upcoming event."""
|
||||
return self.data.event
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Get all events in a specific time frame."""
|
||||
"""Return calendar events within a datetime range."""
|
||||
return await self.data.async_get_events(hass, start_date, end_date)
|
||||
|
|
|
@ -8,7 +8,6 @@ import voluptuous as vol
|
|||
from voluptuous.error import Error as VoluptuousError
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.setup import setup_component
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
|
@ -312,9 +311,6 @@ def do_setup(hass, hass_config, config):
|
|||
setup_services(hass, hass_config, track_new_found_calendars,
|
||||
calendar_service)
|
||||
|
||||
# Ensure component is loaded
|
||||
setup_component(hass, 'calendar', config)
|
||||
|
||||
for calendar in hass.data[DATA_INDEX].values():
|
||||
discovery.load_platform(hass, 'calendar', DOMAIN, calendar,
|
||||
hass_config)
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
"""Support for Google Calendar Search binary sensors."""
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.calendar import (
|
||||
ENTITY_ID_FORMAT, CalendarEventDevice, calculate_offset, is_offset_reached)
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
from . import (
|
||||
CONF_CAL_ID, CONF_ENTITIES, CONF_IGNORE_AVAILABILITY, CONF_SEARCH,
|
||||
CONF_TRACK, TOKEN_FILE, CONF_MAX_RESULTS, GoogleCalendarService)
|
||||
CONF_CAL_ID, CONF_DEVICE_ID, CONF_ENTITIES, CONF_IGNORE_AVAILABILITY,
|
||||
CONF_MAX_RESULTS, CONF_NAME, CONF_OFFSET, CONF_SEARCH, CONF_TRACK,
|
||||
DEFAULT_CONF_OFFSET, TOKEN_FILE, GoogleCalendarService)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -29,27 +33,66 @@ def setup_platform(hass, config, add_entities, disc_info=None):
|
|||
return
|
||||
|
||||
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||
add_entities([GoogleCalendarEventDevice(hass, calendar_service,
|
||||
disc_info[CONF_CAL_ID], data)
|
||||
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
|
||||
entities = []
|
||||
for data in disc_info[CONF_ENTITIES]:
|
||||
if not data[CONF_TRACK]:
|
||||
continue
|
||||
entity_id = generate_entity_id(
|
||||
ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass)
|
||||
entity = GoogleCalendarEventDevice(
|
||||
calendar_service, disc_info[CONF_CAL_ID], data, entity_id)
|
||||
entities.append(entity)
|
||||
|
||||
add_entities(entities, True)
|
||||
|
||||
|
||||
class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||
"""A calendar event device."""
|
||||
|
||||
def __init__(self, hass, calendar_service, calendar, data):
|
||||
def __init__(self, calendar_service, calendar, data, entity_id):
|
||||
"""Create the Calendar event device."""
|
||||
self.data = GoogleCalendarData(calendar_service, calendar,
|
||||
data.get(CONF_SEARCH),
|
||||
data.get(CONF_IGNORE_AVAILABILITY),
|
||||
self.data = GoogleCalendarData(
|
||||
calendar_service, calendar,
|
||||
data.get(CONF_SEARCH), data.get(CONF_IGNORE_AVAILABILITY),
|
||||
data.get(CONF_MAX_RESULTS))
|
||||
self._event = None
|
||||
self._name = data[CONF_NAME]
|
||||
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
||||
self._offset_reached = False
|
||||
self.entity_id = entity_id
|
||||
|
||||
super().__init__(hass, data)
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
return {
|
||||
'offset_reached': self._offset_reached,
|
||||
}
|
||||
|
||||
@property
|
||||
def event(self):
|
||||
"""Return the next upcoming event."""
|
||||
return self._event
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
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)
|
||||
|
||||
def update(self):
|
||||
"""Update event data."""
|
||||
self.data.update()
|
||||
event = copy.deepcopy(self.data.event)
|
||||
if event is None:
|
||||
self._event = event
|
||||
return
|
||||
event = calculate_offset(event, self._offset)
|
||||
self._offset_reached = is_offset_reached(event)
|
||||
self._event = event
|
||||
|
||||
|
||||
class GoogleCalendarData:
|
||||
"""Class to utilize calendar service object to get next event."""
|
||||
|
@ -71,7 +114,7 @@ class GoogleCalendarData:
|
|||
try:
|
||||
service = self.calendar_service.get()
|
||||
except ServerNotFoundError:
|
||||
_LOGGER.warning("Unable to connect to Google, using cached data")
|
||||
_LOGGER.error("Unable to connect to Google")
|
||||
return None, None
|
||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||
params['calendarId'] = self.calendar_id
|
||||
|
@ -87,7 +130,7 @@ class GoogleCalendarData:
|
|||
service, params = await hass.async_add_executor_job(
|
||||
self._prepare_query)
|
||||
if service is None:
|
||||
return
|
||||
return []
|
||||
params['timeMin'] = start_date.isoformat('T')
|
||||
params['timeMax'] = end_date.isoformat('T')
|
||||
|
||||
|
@ -111,7 +154,7 @@ class GoogleCalendarData:
|
|||
"""Get the latest data."""
|
||||
service, params = self._prepare_query()
|
||||
if service is None:
|
||||
return False
|
||||
return
|
||||
params['timeMin'] = dt.now().isoformat('T')
|
||||
|
||||
events = service.events()
|
||||
|
@ -131,4 +174,3 @@ class GoogleCalendarData:
|
|||
break
|
||||
|
||||
self.event = new_event
|
||||
return True
|
||||
|
|
|
@ -6,7 +6,6 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.calendar import (
|
||||
DOMAIN, PLATFORM_SCHEMA, CalendarEventDevice)
|
||||
from homeassistant.components.google import CONF_DEVICE_ID
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
|
@ -148,17 +147,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
label_id_lookup[label[NAME].lower()] = label[ID]
|
||||
|
||||
# Check config for more projects.
|
||||
extra_projects = config.get(CONF_EXTRA_PROJECTS)
|
||||
extra_projects = config[CONF_EXTRA_PROJECTS]
|
||||
for project in extra_projects:
|
||||
# Special filter: By date
|
||||
project_due_date = project.get(CONF_PROJECT_DUE_DATE)
|
||||
|
||||
# Special filter: By label
|
||||
project_label_filter = project.get(CONF_PROJECT_LABEL_WHITELIST)
|
||||
project_label_filter = project[CONF_PROJECT_LABEL_WHITELIST]
|
||||
|
||||
# Special filter: By name
|
||||
# Names must be converted into IDs.
|
||||
project_name_filter = project.get(CONF_PROJECT_WHITELIST)
|
||||
project_name_filter = project[CONF_PROJECT_WHITELIST]
|
||||
project_id_filter = [
|
||||
project_id_lookup[project_name.lower()]
|
||||
for project_name in project_name_filter]
|
||||
|
@ -226,30 +225,26 @@ class TodoistProjectDevice(CalendarEventDevice):
|
|||
data, labels, token, latest_task_due_date,
|
||||
whitelisted_labels, whitelisted_projects
|
||||
)
|
||||
self._cal_data = {}
|
||||
self._name = data[CONF_NAME]
|
||||
|
||||
# Set up the calendar side of things
|
||||
calendar_format = {
|
||||
CONF_NAME: data[CONF_NAME],
|
||||
# Set Entity ID to use the name so we can identify calendars
|
||||
CONF_DEVICE_ID: data[CONF_NAME]
|
||||
}
|
||||
@property
|
||||
def event(self):
|
||||
"""Return the next upcoming event."""
|
||||
return self.data.event
|
||||
|
||||
super().__init__(hass, calendar_format)
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
def update(self):
|
||||
"""Update all Todoist Calendars."""
|
||||
# Set basic calendar data
|
||||
super().update()
|
||||
|
||||
self.data.update()
|
||||
# Set Todoist-specific data that can't easily be grabbed
|
||||
self._cal_data[ALL_TASKS] = [
|
||||
task[SUMMARY] for task in self.data.all_project_tasks]
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up all calendar data."""
|
||||
super().cleanup()
|
||||
self._cal_data[ALL_TASKS] = []
|
||||
|
||||
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)
|
||||
|
@ -259,11 +254,9 @@ class TodoistProjectDevice(CalendarEventDevice):
|
|||
"""Return the device state attributes."""
|
||||
if self.data.event is None:
|
||||
# No tasks, we don't REALLY need to show anything.
|
||||
return {}
|
||||
return None
|
||||
|
||||
attributes = super().device_state_attributes
|
||||
|
||||
# Add additional attributes.
|
||||
attributes = {}
|
||||
attributes[DUE_TODAY] = self.data.event[DUE_TODAY]
|
||||
attributes[OVERDUE] = self.data.event[OVERDUE]
|
||||
attributes[ALL_TASKS] = self._cal_data[ALL_TASKS]
|
||||
|
@ -314,7 +307,7 @@ class TodoistProjectData:
|
|||
self.event = None
|
||||
|
||||
self._api = api
|
||||
self._name = project_data.get(CONF_NAME)
|
||||
self._name = project_data[CONF_NAME]
|
||||
# If no ID is defined, fetch all tasks.
|
||||
self._id = project_data.get(CONF_ID)
|
||||
|
||||
|
@ -487,10 +480,12 @@ class TodoistProjectData:
|
|||
if self._id is None:
|
||||
project_task_data = [
|
||||
task for task in self._api.state[TASKS]
|
||||
if not self._project_id_whitelist or
|
||||
task[PROJECT_ID] in self._project_id_whitelist]
|
||||
if not self._project_id_whitelist
|
||||
or task[PROJECT_ID] in self._project_id_whitelist]
|
||||
else:
|
||||
project_task_data = self._api.projects.get_data(self._id)[TASKS]
|
||||
project_data = await hass.async_add_executor_job(
|
||||
self._api.projects.get_data, self._id)
|
||||
project_task_data = project_data[TASKS]
|
||||
|
||||
events = []
|
||||
time_format = '%a %d %b %Y %H:%M:%S %z'
|
||||
|
@ -515,8 +510,8 @@ class TodoistProjectData:
|
|||
self._api.sync()
|
||||
project_task_data = [
|
||||
task for task in self._api.state[TASKS]
|
||||
if not self._project_id_whitelist or
|
||||
task[PROJECT_ID] in self._project_id_whitelist]
|
||||
if not self._project_id_whitelist
|
||||
or task[PROJECT_ID] in self._project_id_whitelist]
|
||||
else:
|
||||
project_task_data = self._api.projects.get_data(self._id)[TASKS]
|
||||
|
||||
|
@ -524,7 +519,7 @@ class TodoistProjectData:
|
|||
if not project_task_data:
|
||||
_LOGGER.debug("No data for %s", self._name)
|
||||
self.event = None
|
||||
return True
|
||||
return
|
||||
|
||||
# Keep an updated list of all tasks in this project.
|
||||
project_tasks = []
|
||||
|
@ -539,7 +534,7 @@ class TodoistProjectData:
|
|||
# We had no valid tasks
|
||||
_LOGGER.debug("No valid tasks for %s", self._name)
|
||||
self.event = None
|
||||
return True
|
||||
return
|
||||
|
||||
# Make sure the task collection is reset to prevent an
|
||||
# infinite collection repeating the same tasks
|
||||
|
@ -574,4 +569,3 @@ class TodoistProjectData:
|
|||
).strftime(DATE_STR_FORMAT)
|
||||
}
|
||||
_LOGGER.debug("Updated %s", self._name)
|
||||
return True
|
||||
|
|
|
@ -17,6 +17,7 @@ import homeassistant.util.dt as dt_util
|
|||
|
||||
from tests.common import async_mock_service
|
||||
|
||||
|
||||
GOOGLE_CONFIG = {
|
||||
CONF_CLIENT_ID: 'client_id',
|
||||
CONF_CLIENT_SECRET: 'client_secret',
|
||||
|
@ -304,7 +305,7 @@ async def test_all_day_offset_event(hass, mock_next_event):
|
|||
}
|
||||
|
||||
|
||||
async def test_update_false(hass, google_service):
|
||||
async def test_update_error(hass, google_service):
|
||||
"""Test that the calendar handles a server error."""
|
||||
google_service.return_value.get = Mock(
|
||||
side_effect=httplib2.ServerNotFoundError("unit test"))
|
||||
|
|
Loading…
Reference in New Issue