diff --git a/.coveragerc b/.coveragerc
index 09d06ec1082..d9e74d99d78 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -28,6 +28,9 @@ omit =
homeassistant/components/envisalink.py
homeassistant/components/*/envisalink.py
+ homeassistant/components/google.py
+ homeassistant/components/*/google.py
+
homeassistant/components/insteon_hub.py
homeassistant/components/*/insteon_hub.py
@@ -132,7 +135,7 @@ omit =
homeassistant/components/climate/knx.py
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
- homeassistant/components/cover/garadget.py
+ homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/scsgate.py
diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py
new file mode 100644
index 00000000000..503b97a2b13
--- /dev/null
+++ b/homeassistant/components/calendar/__init__.py
@@ -0,0 +1,183 @@
+"""
+Support for Google Calendar event device sensors.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/calendar/
+
+"""
+import logging
+import re
+
+from homeassistant.components.google import (CONF_OFFSET,
+ CONF_DEVICE_ID,
+ CONF_NAME)
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.helpers.config_validation import time_period_str
+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
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'calendar'
+ENTITY_ID_FORMAT = DOMAIN + '.{}'
+
+
+def setup(hass, config):
+ """Track states and offer events for calendars."""
+ component = EntityComponent(
+ logging.getLogger(__name__), DOMAIN, hass, 60, DOMAIN)
+
+ component.setup(config)
+
+ return True
+
+
+DEFAULT_CONF_TRACK_NEW = True
+DEFAULT_CONF_OFFSET = '!!'
+
+
+# pylint: disable=too-many-instance-attributes
+class CalendarEventDevice(Entity):
+ """A calendar event device."""
+
+ # Classes overloading this must set data to an object
+ # with an update() method
+ data = None
+
+ # pylint: disable=too-many-arguments
+ 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):
+ """State Attributes for HA."""
+ 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
+
+ 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),
+ }
+
+ @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
+
+ now = dt.now()
+
+ if start <= now and end > now:
+ 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
+
+ def _get_date(date):
+ """Get the dateTime from date or dateTime as a local."""
+ if 'date' in date:
+ return dt.as_utc(dt.dt.datetime.combine(
+ dt.parse_date(date['date']), dt.dt.time()))
+ else:
+ return dt.parse_datetime(date['dateTime'])
+
+ start = _get_date(self.data.event['start'])
+ end = _get_date(self.data.event['end'])
+
+ summary = self.data.event['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']
diff --git a/homeassistant/components/calendar/demo.py b/homeassistant/components/calendar/demo.py
new file mode 100755
index 00000000000..279119a1ff5
--- /dev/null
+++ b/homeassistant/components/calendar/demo.py
@@ -0,0 +1,82 @@
+"""
+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 homeassistant.util.dt as dt_util
+from homeassistant.components.calendar import CalendarEventDevice
+from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Setup the Demo binary sensor platform."""
+ calendar_data_future = DemoGoogleCalendarDataFuture()
+ calendar_data_current = DemoGoogleCalendarDataCurrent()
+ add_devices([
+ DemoGoogleCalendar(hass, calendar_data_future, {
+ CONF_NAME: 'Future Event',
+ CONF_DEVICE_ID: 'future_event',
+ }),
+
+ DemoGoogleCalendar(hass, calendar_data_current, {
+ CONF_NAME: 'Current Event',
+ CONF_DEVICE_ID: 'current_event',
+ }),
+ ])
+
+
+class DemoGoogleCalendarData(object):
+ """Setup base class for data."""
+
+ # pylint: disable=no-self-use
+ def update(self):
+ """Return true so entity knows we have new data."""
+ return True
+
+
+class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
+ """Setup future data event."""
+
+ def __init__(self):
+ """Set the event to a future event."""
+ one_hour_from_now = dt_util.now() \
+ + dt_util.dt.timedelta(minutes=30)
+ self.event = {
+ 'start': {
+ 'dateTime': one_hour_from_now.isoformat()
+ },
+ 'end': {
+ 'dateTime': (one_hour_from_now + dt_util.dt.
+ timedelta(minutes=60)).isoformat()
+ },
+ 'summary': 'Future Event',
+ }
+
+
+class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
+ """Create a current event we're in the middle of."""
+
+ def __init__(self):
+ """Set the event data."""
+ middle_of_event = dt_util.now() \
+ - dt_util.dt.timedelta(minutes=30)
+ self.event = {
+ 'start': {
+ 'dateTime': middle_of_event.isoformat()
+ },
+ 'end': {
+ 'dateTime': (middle_of_event + dt_util.dt.
+ timedelta(minutes=60)).isoformat()
+ },
+ 'summary': 'Current Event',
+ }
+
+
+class DemoGoogleCalendar(CalendarEventDevice):
+ """A Demo binary sensor."""
+
+ def __init__(self, hass, calendar_data, data):
+ """The same as a google calendar but without the api calls."""
+ self.data = calendar_data
+ super().__init__(hass, data)
diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py
new file mode 100644
index 00000000000..741b3238b49
--- /dev/null
+++ b/homeassistant/components/calendar/google.py
@@ -0,0 +1,79 @@
+"""
+Support for Google Calendar Search binary sensors.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/binary_sensor.google_calendar/
+"""
+# pylint: disable=import-error
+import logging
+from datetime import timedelta
+
+from homeassistant.components.calendar import CalendarEventDevice
+from homeassistant.components.google import (CONF_CAL_ID, CONF_ENTITIES,
+ CONF_TRACK, TOKEN_FILE,
+ GoogleCalendarService)
+from homeassistant.util import Throttle, dt
+
+DEFAULT_GOOGLE_SEARCH_PARAMS = {
+ 'orderBy': 'startTime',
+ 'maxResults': 1,
+ 'singleEvents': True,
+}
+
+# Return cached results if last scan was less then this time ago
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, disc_info=None):
+ """Setup the calendar platform for event devices."""
+ if disc_info is None:
+ return
+
+ if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
+ return
+
+ calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
+ add_devices([GoogleCalendarEventDevice(hass, calendar_service,
+ disc_info[CONF_CAL_ID], data)
+ for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
+
+
+# pylint: disable=too-many-instance-attributes
+class GoogleCalendarEventDevice(CalendarEventDevice):
+ """A calendar event device."""
+
+ def __init__(self, hass, calendar_service, calendar, data):
+ """Create the Calendar event device."""
+ self.data = GoogleCalendarData(calendar_service, calendar,
+ data.get('search', None))
+ super().__init__(hass, data)
+
+
+class GoogleCalendarData(object):
+ """Class to utilize calendar service object to get next event."""
+
+ def __init__(self, calendar_service, calendar_id, search=None):
+ """Setup how we are going to search the google calendar."""
+ self.calendar_service = calendar_service
+ self.calendar_id = calendar_id
+ self.search = search
+ self.event = None
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self):
+ """Get the latest data."""
+ service = self.calendar_service.get()
+ params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
+ params['timeMin'] = dt.utcnow().isoformat('T')
+ params['calendarId'] = self.calendar_id
+ if self.search:
+ params['q'] = self.search
+
+ events = service.events() # pylint: disable=no-member
+ result = events.list(**params).execute()
+
+ items = result.get('items', [])
+ self.event = items[0] if len(items) == 1 else None
+ return True
diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py
index 9f3042320c9..3f3454e0f02 100644
--- a/homeassistant/components/demo.py
+++ b/homeassistant/components/demo.py
@@ -17,6 +17,7 @@ DOMAIN = 'demo'
COMPONENTS_WITH_DEMO_PLATFORM = [
'alarm_control_panel',
'binary_sensor',
+ 'calendar',
'camera',
'climate',
'cover',
diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py
new file mode 100644
index 00000000000..3dbc2c1a1ec
--- /dev/null
+++ b/homeassistant/components/google.py
@@ -0,0 +1,292 @@
+"""
+Support for Google - Calendar Event Devices.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/google/
+
+NOTE TO OTHER DEVELOPERS: IF YOU ADD MORE SCOPES TO THE OAUTH THAN JUST
+CALENDAR THEN USERS WILL NEED TO DELETE THEIR TOKEN_FILE. THEY WILL LOSE THEIR
+REFRESH_TOKEN PIECE WHEN RE-AUTHENTICATING TO ADD MORE API ACCESS
+IT'S BEST TO JUST HAVE SEPARATE OAUTH FOR DIFFERENT PIECES OF GOOGLE
+"""
+import logging
+import os
+import yaml
+
+import voluptuous as vol
+from voluptuous.error import Error as VoluptuousError
+
+import homeassistant.helpers.config_validation as cv
+import homeassistant.loader as loader
+from homeassistant import bootstrap
+from homeassistant.helpers import discovery
+from homeassistant.helpers.entity import generate_entity_id
+from homeassistant.helpers.event import track_time_change
+from homeassistant.util import convert, dt
+
+REQUIREMENTS = [
+ 'google-api-python-client==1.5.5',
+ 'oauth2client==3.0.0',
+]
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'google'
+ENTITY_ID_FORMAT = DOMAIN + '.{}'
+
+CONF_CLIENT_ID = 'client_id'
+CONF_CLIENT_SECRET = 'client_secret'
+CONF_TRACK_NEW = 'track_new_calendar'
+
+CONF_CAL_ID = 'cal_id'
+CONF_DEVICE_ID = 'device_id'
+CONF_NAME = 'name'
+CONF_ENTITIES = 'entities'
+CONF_TRACK = 'track'
+CONF_SEARCH = 'search'
+CONF_OFFSET = 'offset'
+
+DEFAULT_CONF_TRACK_NEW = True
+DEFAULT_CONF_OFFSET = '!!'
+
+NOTIFICATION_ID = 'google_calendar_notification'
+NOTIFICATION_TITLE = 'Google Calendar Setup'
+GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors"
+
+SERVICE_SCAN_CALENDARS = 'scan_for_calendars'
+SERVICE_FOUND_CALENDARS = 'found_calendar'
+
+DATA_INDEX = 'google_calendars'
+
+YAML_DEVICES = '{}_calendars.yaml'.format(DOMAIN)
+SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
+
+TOKEN_FILE = '.{}.token'.format(DOMAIN)
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_CLIENT_ID): cv.string,
+ vol.Required(CONF_CLIENT_SECRET): cv.string,
+ vol.Optional(CONF_TRACK_NEW): cv.boolean,
+ })
+}, extra=vol.ALLOW_EXTRA)
+
+_SINGLE_CALSEARCH_CONFIG = vol.Schema({
+ vol.Required(CONF_NAME): cv.string,
+ vol.Required(CONF_DEVICE_ID): cv.string,
+ vol.Optional(CONF_TRACK): cv.boolean,
+ vol.Optional(CONF_SEARCH): vol.Any(cv.string, None),
+ vol.Optional(CONF_OFFSET): cv.string,
+})
+
+DEVICE_SCHEMA = vol.Schema({
+ vol.Required(CONF_CAL_ID): cv.string,
+ vol.Required(CONF_ENTITIES, None):
+ vol.All(cv.ensure_list, [_SINGLE_CALSEARCH_CONFIG]),
+}, extra=vol.ALLOW_EXTRA)
+
+
+def do_authentication(hass, config):
+ """Notify user of actions and authenticate.
+
+ Notify user of user_code and verification_url then poll
+ until we have an access token.
+ """
+ from oauth2client.client import (
+ OAuth2WebServerFlow,
+ OAuth2DeviceCodeError,
+ FlowExchangeError
+ )
+ from oauth2client.file import Storage
+
+ oauth = OAuth2WebServerFlow(
+ config[CONF_CLIENT_ID],
+ config[CONF_CLIENT_SECRET],
+ 'https://www.googleapis.com/auth/calendar.readonly',
+ 'Home-Assistant.io',
+ )
+
+ persistent_notification = loader.get_component('persistent_notification')
+ try:
+ dev_flow = oauth.step1_get_device_and_user_codes()
+ except OAuth2DeviceCodeError as err:
+ persistent_notification.create(
+ hass, 'Error: {}
You will need to restart hass after fixing.'
+ ''.format(err),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+ return False
+
+ persistent_notification.create(
+ hass, 'In order to authorize Home-Assistant to view your calendars'
+ 'You must visit: {} and enter'
+ 'code: {}'.format(dev_flow.verification_url,
+ dev_flow.verification_url,
+ dev_flow.user_code),
+ title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID
+ )
+
+ def step2_exchange(now):
+ """Keep trying to validate the user_code until it expires."""
+ if now >= dt.as_local(dev_flow.user_code_expiry):
+ persistent_notification.create(
+ hass, 'Authenication code expired, please restart '
+ 'Home-Assistant and try again',
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+ listener()
+
+ try:
+ credentials = oauth.step2_exchange(device_flow_info=dev_flow)
+ except FlowExchangeError:
+ # not ready yet, call again
+ return
+
+ storage = Storage(hass.config.path(TOKEN_FILE))
+ storage.put(credentials)
+ do_setup(hass, config)
+ listener()
+ persistent_notification.create(
+ hass, 'We are all setup now. Check {} for calendars that have '
+ 'been found'.format(YAML_DEVICES),
+ title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID)
+
+ listener = track_time_change(hass, step2_exchange,
+ second=range(0, 60, dev_flow.interval))
+
+ return True
+
+
+def setup(hass, config):
+ """Setup the platform."""
+ if DATA_INDEX not in hass.data:
+ hass.data[DATA_INDEX] = {}
+
+ conf = config.get(DOMAIN, {})
+
+ token_file = hass.config.path(TOKEN_FILE)
+ if not os.path.isfile(token_file):
+ do_authentication(hass, conf)
+ else:
+ do_setup(hass, conf)
+
+ return True
+
+
+def setup_services(hass, track_new_found_calendars, calendar_service):
+ """Setup service listeners."""
+ def _found_calendar(call):
+ """Check if we know about a calendar and generate PLATFORM_DISCOVER."""
+ calendar = get_calendar_info(hass, call.data)
+ if hass.data[DATA_INDEX].get(calendar[CONF_CAL_ID], None) is not None:
+ return
+
+ hass.data[DATA_INDEX].update({calendar[CONF_CAL_ID]: calendar})
+
+ update_config(
+ hass.config.path(YAML_DEVICES),
+ hass.data[DATA_INDEX][calendar[CONF_CAL_ID]]
+ )
+
+ discovery.load_platform(hass, 'calendar', DOMAIN,
+ hass.data[DATA_INDEX][calendar[CONF_CAL_ID]])
+
+ hass.services.register(
+ DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar,
+ None, schema=None)
+
+ def _scan_for_calendars(service):
+ """Scan for new calendars."""
+ service = calendar_service.get()
+ cal_list = service.calendarList() # pylint: disable=no-member
+ calendars = cal_list.list().execute()['items']
+ for calendar in calendars:
+ calendar['track'] = track_new_found_calendars
+ hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS,
+ calendar)
+
+ hass.services.register(
+ DOMAIN, SERVICE_SCAN_CALENDARS,
+ _scan_for_calendars,
+ None, schema=None)
+ return True
+
+
+def do_setup(hass, config):
+ """Run the setup after we have everything configured."""
+ # load calendars the user has configured
+ hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES))
+
+ calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
+ track_new_found_calendars = convert(config.get(CONF_TRACK_NEW),
+ bool, DEFAULT_CONF_TRACK_NEW)
+ setup_services(hass, track_new_found_calendars, calendar_service)
+
+ # Ensure component is loaded
+ bootstrap.setup_component(hass, 'calendar', config)
+
+ for calendar in hass.data[DATA_INDEX].values():
+ discovery.load_platform(hass, 'calendar', DOMAIN, calendar)
+
+ # look for any new calendars
+ hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None)
+ return True
+
+
+class GoogleCalendarService(object):
+ """Calendar service interface to google."""
+
+ def __init__(self, token_file):
+ """We just need the token_file."""
+ self.token_file = token_file
+
+ def get(self):
+ """Get the calendar service from the storage file token."""
+ import httplib2
+ from oauth2client.file import Storage
+ from googleapiclient import discovery as google_discovery
+ credentials = Storage(self.token_file).get()
+ http = credentials.authorize(httplib2.Http())
+ service = google_discovery.build('calendar', 'v3', http=http)
+ return service
+
+
+def get_calendar_info(hass, calendar):
+ """Convert data from Google into DEVICE_SCHEMA."""
+ calendar_info = DEVICE_SCHEMA({
+ CONF_CAL_ID: calendar['id'],
+ CONF_ENTITIES: [{
+ CONF_TRACK: calendar['track'],
+ CONF_NAME: calendar['summary'],
+ CONF_DEVICE_ID: generate_entity_id('{}', calendar['summary'],
+ hass=hass),
+ }]
+ })
+ return calendar_info
+
+
+def load_config(path):
+ """Load the google_calendar_devices.yaml."""
+ calendars = {}
+ try:
+ with open(path) as file:
+ data = yaml.load(file)
+ for calendar in data:
+ try:
+ calendars.update({calendar[CONF_CAL_ID]:
+ DEVICE_SCHEMA(calendar)})
+ except VoluptuousError as exception:
+ # keep going
+ _LOGGER.warning('Calendar Invalid Data: %s', exception)
+ except FileNotFoundError:
+ # When YAML file could not be loaded/did not contain a dict
+ return {}
+
+ return calendars
+
+
+def update_config(path, calendar):
+ """Write the google_calendar_devices.yaml."""
+ with open(path, 'a') as out:
+ out.write('\n')
+ yaml.dump([calendar], out, default_flow_style=False)
diff --git a/requirements_all.txt b/requirements_all.txt
index 70331ae1940..8fe00b301c5 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -124,6 +124,9 @@ fuzzywuzzy==0.14.0
# homeassistant.components.notify.gntp
gntp==1.0.3
+# homeassistant.components.google
+google-api-python-client==1.5.5
+
# homeassistant.components.sensor.google_travel_time
googlemaps==2.4.4
@@ -280,6 +283,9 @@ netdisco==0.7.6
# homeassistant.components.sensor.neurio_energy
neurio==0.2.10
+# homeassistant.components.google
+oauth2client==3.0.0
+
# homeassistant.components.switch.orvibo
orvibo==1.1.1
diff --git a/tests/components/calendar/__init__.py b/tests/components/calendar/__init__.py
new file mode 100644
index 00000000000..4386f422d21
--- /dev/null
+++ b/tests/components/calendar/__init__.py
@@ -0,0 +1 @@
+"""The tests for calendar sensor platforms."""
diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py
new file mode 100644
index 00000000000..534faccd737
--- /dev/null
+++ b/tests/components/calendar/test_google.py
@@ -0,0 +1,421 @@
+"""The tests for the google calendar component."""
+# pylint: disable=protected-access
+import logging
+import unittest
+from unittest.mock import patch
+
+import homeassistant.components.calendar as calendar_base
+import homeassistant.components.calendar.google as calendar
+import homeassistant.util.dt as dt_util
+from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON
+from homeassistant.helpers.template import DATE_STR_FORMAT
+from tests.common import get_test_home_assistant
+
+TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}}
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TestComponentsGoogleCalendar(unittest.TestCase):
+ """Test the Google calendar."""
+
+ hass = None # HomeAssistant
+
+ # pylint: disable=invalid-name
+ def setUp(self):
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+
+ # Set our timezone to CST/Regina so we can check calculations
+ # This keeps UTC-6 all year round
+ dt_util.set_default_time_zone(dt_util.get_time_zone('America/Regina'))
+
+ # pylint: disable=invalid-name
+ def tearDown(self):
+ """Stop everything that was started."""
+ dt_util.set_default_time_zone(dt_util.get_time_zone('UTC'))
+
+ self.hass.stop()
+
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_all_day_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ week_from_today = dt_util.dt.date.today() \
+ + dt_util.dt.timedelta(days=7)
+ event = {
+ 'summary': 'Test All Day Event',
+ 'start': {
+ 'date': week_from_today.isoformat()
+ },
+ 'end': {
+ 'date': (week_from_today + dt_util.dt.timedelta(days=1))
+ .isoformat()
+ },
+ 'location': 'Test Cases',
+ 'description': 'We\'re just testing that all day events get setup '
+ 'correctly',
+ 'kind': 'calendar#event',
+ 'created': '2016-06-23T16:37:57.000Z',
+ 'transparency': 'transparent',
+ 'updated': '2016-06-24T01:57:21.045Z',
+ 'reminders': {'useDefault': True},
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'id': '_c8rinwq863h45qnucyoi43ny8',
+ 'etag': '"2933466882090000"',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ 'iCalUID': 'cydrevtfuybguinhomj@google.com',
+ 'status': 'confirmed'
+ }
+
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test All Day'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None,
+ '', {'name': device_name})
+
+ self.assertEquals(cal.name, device_name)
+
+ self.assertEquals(cal.state, STATE_OFF)
+
+ self.assertFalse(cal.offset_reached())
+
+ self.assertEquals(cal.device_state_attributes, {
+ 'message': event['summary'],
+ 'all_day': True,
+ 'offset_reached': False,
+ 'start_time': '{} 06:00:00'.format(event['start']['date']),
+ 'end_time': '{} 06:00:00'.format(event['end']['date']),
+ 'location': event['location'],
+ 'description': event['description']
+ })
+
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_future_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ one_hour_from_now = dt_util.now() \
+ + dt_util.dt.timedelta(minutes=30)
+ event = {
+ 'start': {
+ 'dateTime': one_hour_from_now.isoformat()
+ },
+ 'end': {
+ 'dateTime': (one_hour_from_now
+ + dt_util.dt.timedelta(minutes=60))
+ .isoformat()
+ },
+ 'summary': 'Test Event in 30 minutes',
+ 'reminders': {'useDefault': True},
+ 'id': 'aioehgni435lihje',
+ 'status': 'confirmed',
+ 'updated': '2016-11-05T15:52:07.329Z',
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True,
+ },
+ 'created': '2016-11-05T15:52:07.000Z',
+ 'iCalUID': 'dsfohuygtfvgbhnuju@google.com',
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ },
+ 'etag': '"2956722254658000"',
+ 'kind': 'calendar#event',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ }
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test Future Event'
+ device_id = 'test_future_event'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
+ {'name': device_name})
+
+ self.assertEquals(cal.name, device_name)
+
+ self.assertEquals(cal.state, STATE_OFF)
+
+ self.assertFalse(cal.offset_reached())
+
+ self.assertEquals(cal.device_state_attributes, {
+ 'message': event['summary'],
+ 'all_day': False,
+ 'offset_reached': False,
+ 'start_time': one_hour_from_now.strftime(DATE_STR_FORMAT),
+ 'end_time':
+ (one_hour_from_now + dt_util.dt.timedelta(minutes=60))
+ .strftime(DATE_STR_FORMAT),
+ 'location': '',
+ 'description': ''
+ })
+
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_in_progress_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ middle_of_event = dt_util.now() \
+ - dt_util.dt.timedelta(minutes=30)
+ event = {
+ 'start': {
+ 'dateTime': middle_of_event.isoformat()
+ },
+ 'end': {
+ 'dateTime': (middle_of_event + dt_util.dt
+ .timedelta(minutes=60))
+ .isoformat()
+ },
+ 'summary': 'Test Event in Progress',
+ 'reminders': {'useDefault': True},
+ 'id': 'aioehgni435lihje',
+ 'status': 'confirmed',
+ 'updated': '2016-11-05T15:52:07.329Z',
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True,
+ },
+ 'created': '2016-11-05T15:52:07.000Z',
+ 'iCalUID': 'dsfohuygtfvgbhnuju@google.com',
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ },
+ 'etag': '"2956722254658000"',
+ 'kind': 'calendar#event',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ }
+
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test Event in Progress'
+ device_id = 'test_event_in_progress'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
+ {'name': device_name})
+
+ self.assertEquals(cal.name, device_name)
+
+ self.assertEquals(cal.state, STATE_ON)
+
+ self.assertFalse(cal.offset_reached())
+
+ self.assertEquals(cal.device_state_attributes, {
+ 'message': event['summary'],
+ 'all_day': False,
+ 'offset_reached': False,
+ 'start_time': middle_of_event.strftime(DATE_STR_FORMAT),
+ 'end_time':
+ (middle_of_event + dt_util.dt.timedelta(minutes=60))
+ .strftime(DATE_STR_FORMAT),
+ 'location': '',
+ 'description': ''
+ })
+
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_offset_in_progress_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ middle_of_event = dt_util.now() \
+ + dt_util.dt.timedelta(minutes=14)
+ event_summary = 'Test Event in Progress'
+ event = {
+ 'start': {
+ 'dateTime': middle_of_event.isoformat()
+ },
+ 'end': {
+ 'dateTime': (middle_of_event + dt_util.dt
+ .timedelta(minutes=60))
+ .isoformat()
+ },
+ 'summary': '{} !!-15'.format(event_summary),
+ 'reminders': {'useDefault': True},
+ 'id': 'aioehgni435lihje',
+ 'status': 'confirmed',
+ 'updated': '2016-11-05T15:52:07.329Z',
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True,
+ },
+ 'created': '2016-11-05T15:52:07.000Z',
+ 'iCalUID': 'dsfohuygtfvgbhnuju@google.com',
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ },
+ 'etag': '"2956722254658000"',
+ 'kind': 'calendar#event',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ }
+
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test Event in Progress'
+ device_id = 'test_event_in_progress'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
+ {'name': device_name})
+
+ self.assertEquals(cal.name, device_name)
+
+ self.assertEquals(cal.state, STATE_OFF)
+
+ self.assertTrue(cal.offset_reached())
+
+ self.assertEquals(cal.device_state_attributes, {
+ 'message': event_summary,
+ 'all_day': False,
+ 'offset_reached': True,
+ 'start_time': middle_of_event.strftime(DATE_STR_FORMAT),
+ 'end_time':
+ (middle_of_event + dt_util.dt.timedelta(minutes=60))
+ .strftime(DATE_STR_FORMAT),
+ 'location': '',
+ 'description': ''
+ })
+
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_all_day_offset_in_progress_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ tomorrow = dt_util.dt.date.today() \
+ + dt_util.dt.timedelta(days=1)
+
+ offset_hours = (25 - dt_util.now().hour)
+ event_summary = 'Test All Day Event Offset In Progress'
+ event = {
+ 'summary': '{} !!-{}:0'.format(event_summary, offset_hours),
+ 'start': {
+ 'date': tomorrow.isoformat()
+ },
+ 'end': {
+ 'date': (tomorrow + dt_util.dt.timedelta(days=1))
+ .isoformat()
+ },
+ 'location': 'Test Cases',
+ 'description': 'We\'re just testing that all day events get setup '
+ 'correctly',
+ 'kind': 'calendar#event',
+ 'created': '2016-06-23T16:37:57.000Z',
+ 'transparency': 'transparent',
+ 'updated': '2016-06-24T01:57:21.045Z',
+ 'reminders': {'useDefault': True},
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'id': '_c8rinwq863h45qnucyoi43ny8',
+ 'etag': '"2933466882090000"',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ 'iCalUID': 'cydrevtfuybguinhomj@google.com',
+ 'status': 'confirmed'
+ }
+
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test All Day Offset In Progress'
+ device_id = 'test_all_day_offset_in_progress'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
+ {'name': device_name})
+
+ self.assertEquals(cal.name, device_name)
+
+ self.assertEquals(cal.state, STATE_OFF)
+
+ self.assertTrue(cal.offset_reached())
+
+ self.assertEquals(cal.device_state_attributes, {
+ 'message': event_summary,
+ 'all_day': True,
+ 'offset_reached': True,
+ 'start_time': '{} 06:00:00'.format(event['start']['date']),
+ 'end_time': '{} 06:00:00'.format(event['end']['date']),
+ 'location': event['location'],
+ 'description': event['description']
+ })
+
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_all_day_offset_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ tomorrow = dt_util.dt.date.today() \
+ + dt_util.dt.timedelta(days=2)
+
+ offset_hours = (1 + dt_util.now().hour)
+ event_summary = 'Test All Day Event Offset'
+ event = {
+ 'summary': '{} !!-{}:0'.format(event_summary, offset_hours),
+ 'start': {
+ 'date': tomorrow.isoformat()
+ },
+ 'end': {
+ 'date': (tomorrow + dt_util.dt.timedelta(days=1))
+ .isoformat()
+ },
+ 'location': 'Test Cases',
+ 'description': 'We\'re just testing that all day events get setup '
+ 'correctly',
+ 'kind': 'calendar#event',
+ 'created': '2016-06-23T16:37:57.000Z',
+ 'transparency': 'transparent',
+ 'updated': '2016-06-24T01:57:21.045Z',
+ 'reminders': {'useDefault': True},
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'id': '_c8rinwq863h45qnucyoi43ny8',
+ 'etag': '"2933466882090000"',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ 'iCalUID': 'cydrevtfuybguinhomj@google.com',
+ 'status': 'confirmed'
+ }
+
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test All Day Offset'
+ device_id = 'test_all_day_offset'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
+ {'name': device_name})
+
+ self.assertEquals(cal.name, device_name)
+
+ self.assertEquals(cal.state, STATE_OFF)
+
+ self.assertFalse(cal.offset_reached())
+
+ self.assertEquals(cal.device_state_attributes, {
+ 'message': event_summary,
+ 'all_day': True,
+ 'offset_reached': False,
+ 'start_time': '{} 06:00:00'.format(event['start']['date']),
+ 'end_time': '{} 06:00:00'.format(event['end']['date']),
+ 'location': event['location'],
+ 'description': event['description']
+ })
diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py
new file mode 100644
index 00000000000..164c3f57f52
--- /dev/null
+++ b/tests/components/calendar/test_init.py
@@ -0,0 +1 @@
+"""The tests for the calendar component."""
diff --git a/tests/components/test_google.py b/tests/components/test_google.py
new file mode 100644
index 00000000000..aaaaf8a9983
--- /dev/null
+++ b/tests/components/test_google.py
@@ -0,0 +1,90 @@
+"""The tests for the Google Calendar component."""
+import logging
+import unittest
+
+import homeassistant.components.google as google
+from homeassistant.bootstrap import setup_component
+from tests.common import get_test_home_assistant
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TestGoogle(unittest.TestCase):
+ """Test the Google component."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ def test_setup_component(self):
+ """Test setup component."""
+ config = {
+ 'google': {
+ 'client_id': 'id',
+ 'client_secret': 'secret',
+ }
+ }
+
+ self.assertTrue(setup_component(self.hass, 'google', config))
+
+ def test_get_calendar_info(self):
+ calendar = {
+ 'id': 'qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com',
+ 'etag': '"3584134138943410"',
+ 'timeZone': 'UTC',
+ 'accessRole': 'reader',
+ 'foregroundColor': '#000000',
+ 'selected': True,
+ 'kind': 'calendar#calendarListEntry',
+ 'backgroundColor': '#16a765',
+ 'description': 'Test Calendar',
+ 'summary': 'We are, we are, a... Test Calendar',
+ 'colorId': '8',
+ 'defaultReminders': [],
+ 'track': True
+ }
+
+ calendar_info = google.get_calendar_info(self.hass, calendar)
+ self.assertEquals(calendar_info, {
+ 'cal_id': 'qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com',
+ 'entities': [{
+ 'device_id': 'we_are_we_are_a_test_calendar',
+ 'name': 'We are, we are, a... Test Calendar',
+ 'track': True,
+ }]
+ })
+
+ def test_found_calendar(self):
+ calendar = {
+ 'id': 'qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com',
+ 'etag': '"3584134138943410"',
+ 'timeZone': 'UTC',
+ 'accessRole': 'reader',
+ 'foregroundColor': '#000000',
+ 'selected': True,
+ 'kind': 'calendar#calendarListEntry',
+ 'backgroundColor': '#16a765',
+ 'description': 'Test Calendar',
+ 'summary': 'We are, we are, a... Test Calendar',
+ 'colorId': '8',
+ 'defaultReminders': [],
+ 'track': True
+ }
+
+ # self.assertIsInstance(self.hass.data[google.DATA_INDEX], dict)
+ # self.assertEquals(self.hass.data[google.DATA_INDEX], {})
+
+ calendar_service = google.GoogleCalendarService(
+ self.hass.config.path(google.TOKEN_FILE))
+ self.assertTrue(google.setup_services(self.hass, True,
+ calendar_service))
+ self.hass.services.call('google', 'found_calendar', calendar,
+ blocking=True)
+
+ # TODO: Fix this
+ # self.assertTrue(self.hass.data[google.DATA_INDEX]
+ # # .get(calendar['id'], None) is not None)