diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 5198381b976..f5e1d581891 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -9,6 +9,8 @@ import logging from datetime import timedelta import re +from aiohttp import web + from homeassistant.components.google import ( CONF_OFFSET, CONF_DEVICE_ID, CONF_NAME) from homeassistant.const import STATE_OFF, STATE_ON @@ -18,11 +20,15 @@ from homeassistant.helpers.entity import Entity, generate_entity_id 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' +DEPENDENCIES = ['http'] + ENTITY_ID_FORMAT = DOMAIN + '.{}' SCAN_INTERVAL = timedelta(seconds=60) @@ -34,6 +40,8 @@ def async_setup(hass, config): component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DOMAIN) + hass.http.register_view(CalendarEventView(component)) + yield from component.async_setup(config) return True @@ -42,6 +50,14 @@ 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: + return dt.start_of_local_day(dt.dt.datetime.combine( + dt.parse_date(date['date']), dt.dt.time.min)) + return dt.as_local(dt.parse_datetime(date['dateTime'])) + + # pylint: disable=too-many-instance-attributes class CalendarEventDevice(Entity): """A calendar event device.""" @@ -144,15 +160,8 @@ class CalendarEventDevice(Entity): self.cleanup() return - def _get_date(date): - """Get the dateTime from date or dateTime as a local.""" - if 'date' in date: - return dt.start_of_local_day(dt.dt.datetime.combine( - dt.parse_date(date['date']), dt.dt.time.min)) - return dt.as_local(dt.parse_datetime(date['dateTime'])) - - start = _get_date(self.data.event['start']) - end = _get_date(self.data.event['end']) + start = get_date(self.data.event['start']) + end = get_date(self.data.event['end']) summary = self.data.event.get('summary', '') @@ -176,10 +185,37 @@ class CalendarEventDevice(Entity): # 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'] + + +class CalendarEventView(http.HomeAssistantView): + """View to retrieve calendar content.""" + + url = '/api/calendar/{entity_id}' + name = 'api:calendar' + + def __init__(self, component): + """Initialize calendar view.""" + self.component = component + + async def get(self, request, entity_id): + """Return calendar events.""" + entity = self.component.get_entity('calendar.' + entity_id) + start = request.query.get('start') + end = request.query.get('end') + if None in (start, end, entity): + return web.Response(status=400) + try: + start_date = dt.parse_datetime(start) + end_date = dt.parse_datetime(end) + except (ValueError, AttributeError): + return web.Response(status=400) + event_list = await entity.async_get_events(request.app['hass'], + start_date, + end_date) + return self.json(event_list) diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index 6f92891c551..9c30d1481f8 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -11,7 +11,7 @@ import re import voluptuous as vol from homeassistant.components.calendar import ( - PLATFORM_SCHEMA, CalendarEventDevice) + PLATFORM_SCHEMA, CalendarEventDevice, get_date) from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME) import homeassistant.helpers.config_validation as cv @@ -92,7 +92,7 @@ def setup_platform(hass, config, add_devices, disc_info=None): if not config.get(CONF_CUSTOM_CALENDARS): device_data = { CONF_NAME: calendar.name, - CONF_DEVICE_ID: calendar.name + CONF_DEVICE_ID: calendar.name, } calendar_devices.append( WebDavCalendarEventDevice(hass, device_data, calendar) @@ -120,6 +120,10 @@ class WebDavCalendarEventDevice(CalendarEventDevice): attributes = super().device_state_attributes return attributes + 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) + class WebDavCalendarData(object): """Class to utilize the calendar dav client object to get next event.""" @@ -131,6 +135,33 @@ class WebDavCalendarData(object): self.search = search self.event = None + 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 + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" diff --git a/homeassistant/components/calendar/demo.py b/homeassistant/components/calendar/demo.py index 7823f03c85e..5ddd9fe8e3d 100644 --- a/homeassistant/components/calendar/demo.py +++ b/homeassistant/components/calendar/demo.py @@ -4,8 +4,10 @@ Demo platform that has two fake binary sensors. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ +import copy + import homeassistant.util.dt as dt_util -from homeassistant.components.calendar import CalendarEventDevice +from homeassistant.components.calendar import CalendarEventDevice, get_date from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME @@ -16,12 +18,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([ DemoGoogleCalendar(hass, calendar_data_future, { CONF_NAME: 'Future Event', - CONF_DEVICE_ID: 'future_event', + CONF_DEVICE_ID: 'calendar_1', }), DemoGoogleCalendar(hass, calendar_data_current, { CONF_NAME: 'Current Event', - CONF_DEVICE_ID: 'current_event', + CONF_DEVICE_ID: 'calendar_2', }), ]) @@ -29,11 +31,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DemoGoogleCalendarData(object): """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 + async def async_get_events(self, hass, start_date, end_date): + """Get all events in a specific time frame.""" + event = copy.copy(self.event) + event['title'] = event['summary'] + event['start'] = get_date(event['start']).isoformat() + event['end'] = get_date(event['end']).isoformat() + return [event] + class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData): """Representation of a Demo Calendar for a future event.""" @@ -80,3 +92,7 @@ class DemoGoogleCalendar(CalendarEventDevice): """Initialize Google Calendar but without the API calls.""" self.data = calendar_data super().__init__(hass, data) + + 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) diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index 6c26c65ebe7..da76530a36d 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -51,6 +51,10 @@ class GoogleCalendarEventDevice(CalendarEventDevice): super().__init__(hass, data) + 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) + class GoogleCalendarData(object): """Class to utilize calendar service object to get next event.""" @@ -64,9 +68,7 @@ class GoogleCalendarData(object): self.ignore_availability = ignore_availability self.event = None - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data.""" + def _prepare_query(self): from httplib2 import ServerNotFoundError try: @@ -74,13 +76,41 @@ class GoogleCalendarData(object): except ServerNotFoundError: _LOGGER.warning("Unable to connect to Google, using cached data") return False - params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) - params['timeMin'] = dt.now().isoformat('T') params['calendarId'] = self.calendar_id if self.search: params['q'] = self.search + return service, params + + async def async_get_events(self, hass, start_date, end_date): + """Get all events in a specific time frame.""" + service, params = await hass.async_add_job(self._prepare_query) + params['timeMin'] = start_date.isoformat('T') + params['timeMax'] = end_date.isoformat('T') + + # pylint: disable=no-member + events = await hass.async_add_job(service.events) + # pylint: enable=no-member + result = await hass.async_add_job(events.list(**params).execute) + + items = result.get('items', []) + event_list = [] + for item in items: + if (not self.ignore_availability + and 'transparency' in item.keys()): + if item['transparency'] == 'opaque': + event_list.append(item) + else: + event_list.append(item) + return event_list + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data.""" + service, params = self._prepare_query() + params['timeMin'] = dt.now().isoformat('T') + events = service.events() # pylint: disable=no-member result = events.list(**params).execute() diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index b70e44456db..71a6a17de10 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -257,6 +257,10 @@ class TodoistProjectDevice(CalendarEventDevice): 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) + @property def device_state_attributes(self): """Return the device state attributes.""" @@ -485,6 +489,31 @@ class TodoistProjectData(object): continue return event + async def async_get_events(self, hass, start_date, end_date): + """Get all tasks in a specific time frame.""" + 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] + else: + project_task_data = self._api.projects.get_data(self._id)[TASKS] + + events = [] + time_format = '%a %d %b %Y %H:%M:%S %z' + for task in project_task_data: + due_date = datetime.strptime(task['due_date_utc'], time_format) + if due_date > start_date and due_date < end_date: + event = { + 'uid': task['id'], + 'title': task['content'], + 'start': due_date.isoformat(), + 'end': due_date.isoformat(), + 'allDay': True, + } + events.append(event) + return events + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" diff --git a/tests/components/calendar/test_caldav.py b/tests/components/calendar/test_caldav.py index 11dd0cb9635..c5dadbc56ea 100644 --- a/tests/components/calendar/test_caldav.py +++ b/tests/components/calendar/test_caldav.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) DEVICE_DATA = { "name": "Private Calendar", - "device_id": "Private Calendar" + "device_id": "Private Calendar", } EVENTS = [ @@ -163,6 +163,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): def setUp(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.http = Mock() self.calendar = _mock_calendar("Private") # pylint: disable=invalid-name @@ -255,7 +256,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 17:00:00", "end_time": "2017-11-27 18:00:00", "location": "Hamburg", - "description": "Surprisingly rainy" + "description": "Surprisingly rainy", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 30)) @@ -274,7 +275,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 17:00:00", "end_time": "2017-11-27 18:00:00", "location": "Hamburg", - "description": "Surprisingly rainy" + "description": "Surprisingly rainy", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 00)) @@ -293,7 +294,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 16:30:00", "description": "Sunny day", "end_time": "2017-11-27 17:30:00", - "location": "San Francisco" + "location": "San Francisco", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(8, 30)) @@ -311,7 +312,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 10:00:00", "end_time": "2017-11-27 11:00:00", "location": "Hamburg", - "description": "Surprisingly shiny" + "description": "Surprisingly shiny", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(12, 00)) @@ -332,7 +333,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 17:00:00", "end_time": "2017-11-27 18:00:00", "location": "Hamburg", - "description": "Surprisingly rainy" + "description": "Surprisingly rainy", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(12, 00)) @@ -353,7 +354,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 17:00:00", "end_time": "2017-11-27 18:00:00", "location": "Hamburg", - "description": "Surprisingly rainy" + "description": "Surprisingly rainy", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(20, 00)) @@ -395,5 +396,5 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 00:00:00", "end_time": "2017-11-28 00:00:00", "location": "Hamburg", - "description": "What a beautiful day" + "description": "What a beautiful day", }) diff --git a/tests/components/calendar/test_demo.py b/tests/components/calendar/test_demo.py new file mode 100644 index 00000000000..50ac63121b1 --- /dev/null +++ b/tests/components/calendar/test_demo.py @@ -0,0 +1,24 @@ +"""The tests for the demo calendar component.""" +from datetime import timedelta + +from homeassistant.bootstrap import async_setup_component +import homeassistant.util.dt as dt_util + + +async def test_api_calendar_demo_view(hass, aiohttp_client): + """Test the calendar demo view.""" + await async_setup_component(hass, 'calendar', + {'calendar': {'platform': 'demo'}}) + client = await aiohttp_client(hass.http.app) + response = await client.get( + '/api/calendar/calendar_2') + assert response.status == 400 + start = dt_util.now() + end = start + timedelta(days=1) + response = await client.get( + '/api/calendar/calendar_1?start={}&end={}'.format(start.isoformat(), + end.isoformat())) + assert response.status == 200 + events = await response.json() + assert events[0]['summary'] == 'Future Event' + assert events[0]['title'] == 'Future Event' diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index 9f94ea9f44c..d176cd758b4 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -27,6 +27,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.http = Mock() # Set our timezone to CST/Regina so we can check calculations # This keeps UTC-6 all year round @@ -99,7 +100,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'start_time': '{} 00:00:00'.format(event['start']['date']), 'end_time': '{} 00:00:00'.format(event['end']['date']), 'location': event['location'], - 'description': event['description'] + 'description': event['description'], }) @patch('homeassistant.components.calendar.google.GoogleCalendarData') @@ -160,7 +161,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): (one_hour_from_now + dt_util.dt.timedelta(minutes=60)) .strftime(DATE_STR_FORMAT), 'location': '', - 'description': '' + 'description': '', }) @patch('homeassistant.components.calendar.google.GoogleCalendarData') @@ -222,7 +223,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): (middle_of_event + dt_util.dt.timedelta(minutes=60)) .strftime(DATE_STR_FORMAT), 'location': '', - 'description': '' + 'description': '', }) @patch('homeassistant.components.calendar.google.GoogleCalendarData') @@ -285,7 +286,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): (middle_of_event + dt_util.dt.timedelta(minutes=60)) .strftime(DATE_STR_FORMAT), 'location': '', - 'description': '' + 'description': '', }) @pytest.mark.skip @@ -352,7 +353,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'start_time': '{} 06:00:00'.format(event['start']['date']), 'end_time': '{} 06:00:00'.format(event['end']['date']), 'location': event['location'], - 'description': event['description'] + 'description': event['description'], }) @patch('homeassistant.components.calendar.google.GoogleCalendarData') @@ -419,7 +420,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'start_time': '{} 00:00:00'.format(event['start']['date']), 'end_time': '{} 00:00:00'.format(event['end']['date']), 'location': event['location'], - 'description': event['description'] + 'description': event['description'], }) @MockDependency("httplib2")