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:
parent
1128104281
commit
3cd4cb741c
8 changed files with 202 additions and 34 deletions
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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",
|
||||
})
|
||||
|
|
24
tests/components/calendar/test_demo.py
Normal file
24
tests/components/calendar/test_demo.py
Normal 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'
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Reference in a new issue