diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 594a6473877..befa1f40843 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -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,10 +194,12 @@ 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) - or self.include_all_day) + 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 @@ -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,12 +226,13 @@ class WebDavCalendarData: 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)) + 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): diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 5a1ce79c18c..b02511470a4 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -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,70 +45,106 @@ def get_date(date): return dt.as_local(dt.parse_datetime(date['dateTime'])) +def normalize_event(event): + """Normalize a calendar event.""" + normalized_event = {} + + 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 + + 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 + + # 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'] + + return normalized_event + + +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(offset) + search = re.search(reg, summary) + if search and search.group(1): + time = search.group(1) + if ':' not in time: + if time[0] == '+' or time[0] == '-': + time = '{}0:{}'.format(time[0], time[1:]) + else: + time = '0:{}'.format(time) + + offset_time = time_period_str(time) + summary = ( + summary[:search.start()] + summary[search.end():]).strip() + event['summary'] = summary + else: + offset_time = dt.dt.timedelta() # default it + + 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.""" - # Classes overloading this must set data to an object - # with an update() method - data = None - - 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 event(self): + """Return the next upcoming event.""" + raise NotImplementedError() @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 + def state_attributes(self): + """Return the entity state attributes.""" + event = self.event + if event is None: + return None + event = normalize_event(event) 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), + '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.""" - start = self._cal_data.get('start', None) - end = self._cal_data.get('end', None) + 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 @@ -127,65 +153,11 @@ class CalendarEventDevice(Entity): 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', '') - - # 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) - search = re.search(reg, summary) - if search and search.group(1): - time = search.group(1) - if ':' not in time: - if time[0] == '+' or time[0] == '-': - time = '{}0:{}'.format(time[0], time[1:]) - else: - time = '0:{}'.format(time) - - offset_time = time_period_str(time) - summary = (summary[:search.start()] + summary[search.end():]) \ - .strip() - 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'] + 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, diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 6096f8247c4..9a685357b10 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -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) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 027a6b2f568..dc1c7b1d5e5 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -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) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 993c24d8653..66d6b61f75b 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -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), - data.get(CONF_MAX_RESULTS)) + 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 diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 2ee88080924..277377ba633 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -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 diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 1da7b4c55fb..89bd1ec83b8 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -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"))