2017-08-29 20:40:08 +00:00
|
|
|
"""Component to integrate the Home Assistant cloud."""
|
|
|
|
import asyncio
|
2017-11-15 07:16:19 +00:00
|
|
|
from datetime import datetime
|
2017-10-15 02:43:14 +00:00
|
|
|
import json
|
2017-08-29 20:40:08 +00:00
|
|
|
import logging
|
2017-10-15 02:43:14 +00:00
|
|
|
import os
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
|
2017-10-29 11:32:02 +00:00
|
|
|
from homeassistant.const import (
|
|
|
|
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE)
|
2017-11-15 07:16:19 +00:00
|
|
|
from homeassistant.util import dt as dt_util
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-10-15 02:43:14 +00:00
|
|
|
from . import http_api, iot
|
|
|
|
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-10-15 02:43:14 +00:00
|
|
|
REQUIREMENTS = ['warrant==0.5.0']
|
2017-10-29 11:32:02 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2017-10-15 02:43:14 +00:00
|
|
|
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
|
|
|
|
CONF_RELAYER = 'relayer'
|
2017-10-29 11:32:02 +00:00
|
|
|
CONF_USER_POOL_ID = 'user_pool_id'
|
|
|
|
|
2017-08-29 20:40:08 +00:00
|
|
|
MODE_DEV = 'development'
|
|
|
|
DEFAULT_MODE = MODE_DEV
|
2017-10-29 11:32:02 +00:00
|
|
|
DEPENDENCIES = ['http']
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
|
|
DOMAIN: vol.Schema({
|
|
|
|
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
|
2017-10-15 02:43:14 +00:00
|
|
|
vol.In([MODE_DEV] + list(SERVERS)),
|
|
|
|
# Change to optional when we include real servers
|
|
|
|
vol.Required(CONF_COGNITO_CLIENT_ID): str,
|
|
|
|
vol.Required(CONF_USER_POOL_ID): str,
|
|
|
|
vol.Required(CONF_REGION): str,
|
|
|
|
vol.Required(CONF_RELAYER): str,
|
2017-08-29 20:40:08 +00:00
|
|
|
}),
|
|
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_setup(hass, config):
|
|
|
|
"""Initialize the Home Assistant cloud."""
|
|
|
|
if DOMAIN in config:
|
2017-10-15 02:43:14 +00:00
|
|
|
kwargs = config[DOMAIN]
|
|
|
|
else:
|
|
|
|
kwargs = {CONF_MODE: DEFAULT_MODE}
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-10-15 02:43:14 +00:00
|
|
|
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-10-15 02:43:14 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def init_cloud(event):
|
|
|
|
"""Initialize connection."""
|
|
|
|
yield from cloud.initialize()
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-10-15 02:43:14 +00:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, init_cloud)
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
yield from http_api.async_setup(hass)
|
|
|
|
return True
|
2017-10-15 02:43:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Cloud:
|
|
|
|
"""Store the configuration of the cloud connection."""
|
|
|
|
|
|
|
|
def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None,
|
|
|
|
region=None, relayer=None):
|
|
|
|
"""Create an instance of Cloud."""
|
|
|
|
self.hass = hass
|
|
|
|
self.mode = mode
|
|
|
|
self.id_token = None
|
|
|
|
self.access_token = None
|
|
|
|
self.refresh_token = None
|
|
|
|
self.iot = iot.CloudIoT(self)
|
|
|
|
|
|
|
|
if mode == MODE_DEV:
|
|
|
|
self.cognito_client_id = cognito_client_id
|
|
|
|
self.user_pool_id = user_pool_id
|
|
|
|
self.region = region
|
|
|
|
self.relayer = relayer
|
|
|
|
|
|
|
|
else:
|
|
|
|
info = SERVERS[mode]
|
|
|
|
|
|
|
|
self.cognito_client_id = info['cognito_client_id']
|
|
|
|
self.user_pool_id = info['user_pool_id']
|
|
|
|
self.region = info['region']
|
|
|
|
self.relayer = info['relayer']
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_logged_in(self):
|
|
|
|
"""Get if cloud is logged in."""
|
2017-11-15 07:16:19 +00:00
|
|
|
return self.id_token is not None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def subscription_expired(self):
|
|
|
|
"""Return a boolen if the subscription has expired."""
|
|
|
|
# For now, don't enforce subscriptions to exist
|
|
|
|
if 'custom:sub-exp' not in self.claims:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return dt_util.utcnow() > self.expiration_date
|
|
|
|
|
|
|
|
@property
|
|
|
|
def expiration_date(self):
|
|
|
|
"""Return the subscription expiration as a UTC datetime object."""
|
|
|
|
return datetime.combine(
|
|
|
|
dt_util.parse_date(self.claims['custom:sub-exp']),
|
|
|
|
datetime.min.time()).replace(tzinfo=dt_util.UTC)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def claims(self):
|
|
|
|
"""Get the claims from the id token."""
|
|
|
|
from jose import jwt
|
|
|
|
return jwt.get_unverified_claims(self.id_token)
|
2017-10-15 02:43:14 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def user_info_path(self):
|
|
|
|
"""Get path to the stored auth."""
|
|
|
|
return self.path('{}_auth.json'.format(self.mode))
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def initialize(self):
|
|
|
|
"""Initialize and load cloud info."""
|
|
|
|
def load_config():
|
|
|
|
"""Load the configuration."""
|
|
|
|
# Ensure config dir exists
|
|
|
|
path = self.hass.config.path(CONFIG_DIR)
|
|
|
|
if not os.path.isdir(path):
|
|
|
|
os.mkdir(path)
|
|
|
|
|
|
|
|
user_info = self.user_info_path
|
|
|
|
if os.path.isfile(user_info):
|
|
|
|
with open(user_info, 'rt') as file:
|
|
|
|
info = json.loads(file.read())
|
|
|
|
self.id_token = info['id_token']
|
|
|
|
self.access_token = info['access_token']
|
|
|
|
self.refresh_token = info['refresh_token']
|
|
|
|
|
|
|
|
yield from self.hass.async_add_job(load_config)
|
|
|
|
|
2017-11-15 07:16:19 +00:00
|
|
|
if self.id_token is not None:
|
2017-10-15 02:43:14 +00:00
|
|
|
yield from self.iot.connect()
|
|
|
|
|
|
|
|
def path(self, *parts):
|
2017-11-15 07:16:19 +00:00
|
|
|
"""Get config path inside cloud dir.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2017-10-15 02:43:14 +00:00
|
|
|
return self.hass.config.path(CONFIG_DIR, *parts)
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def logout(self):
|
|
|
|
"""Close connection and remove all credentials."""
|
|
|
|
yield from self.iot.disconnect()
|
|
|
|
|
|
|
|
self.id_token = None
|
|
|
|
self.access_token = None
|
|
|
|
self.refresh_token = None
|
|
|
|
|
|
|
|
yield from self.hass.async_add_job(
|
|
|
|
lambda: os.remove(self.user_info_path))
|
|
|
|
|
|
|
|
def write_user_info(self):
|
|
|
|
"""Write user info to a file."""
|
|
|
|
with open(self.user_info_path, 'wt') as file:
|
|
|
|
file.write(json.dumps({
|
|
|
|
'id_token': self.id_token,
|
|
|
|
'access_token': self.access_token,
|
|
|
|
'refresh_token': self.refresh_token,
|
|
|
|
}, indent=4))
|