1
mirror of https://github.com/home-assistant/core synced 2024-07-21 14:24:50 +02:00

Add Calendar API endpoint to get events (#14702)

* Add Calendar API endpoint to get events

* Set default event color

* Fix PR comments

* Fix PR comments

* Fix PR comments

* Remote local.py file

* Use iso 8601

* Fix lint

* Fix PR comments

* Fix PR comments

* Add Support for todoist and demo calendar

* Todoist events are allday events

* Add calendar demo api endpoint test

* Register only one api endpoint for calendar

* Rename demo calendar
This commit is contained in:
Thibault Cohen 2018-06-15 11:16:31 -04:00 committed by Paulus Schoutsen
parent 1128104281
commit 3cd4cb741c
8 changed files with 202 additions and 34 deletions

View File

@ -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)

View File

@ -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."""

View File

@ -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)

View File

@ -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()

View File

@ -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."""

View File

@ -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",
})

View File

@ -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'

View File

@ -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")