Skip to content
9 changes: 7 additions & 2 deletions homeassistant/components/cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from homeassistant.components.google_assistant import helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c

from . import http_api, iot, auth_api, prefs
from . import http_api, iot, auth_api, prefs, cloudhooks
from .const import CONFIG_DIR, DOMAIN, SERVERS

REQUIREMENTS = ['warrant==0.6.1']
Expand All @@ -37,6 +37,7 @@
CONF_USER_POOL_ID = 'user_pool_id'
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url'
CONF_CLOUDHOOK_CREATE_URL = 'cloudhook_create_url'

DEFAULT_MODE = 'production'
DEPENDENCIES = ['http']
Expand Down Expand Up @@ -78,6 +79,7 @@
vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
vol.Optional(CONF_SUBSCRIPTION_INFO_URL): str,
vol.Optional(CONF_CLOUDHOOK_CREATE_URL): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}),
Expand Down Expand Up @@ -113,7 +115,7 @@ class Cloud:
def __init__(self, hass, mode, alexa, google_actions,
cognito_client_id=None, user_pool_id=None, region=None,
relayer=None, google_actions_sync_url=None,
subscription_info_url=None):
subscription_info_url=None, cloudhook_create_url=None):
"""Create an instance of Cloud."""
self.hass = hass
self.mode = mode
Expand All @@ -125,6 +127,7 @@ def __init__(self, hass, mode, alexa, google_actions,
self.access_token = None
self.refresh_token = None
self.iot = iot.CloudIoT(self)
self.cloudhooks = cloudhooks.Cloudhooks(self)

if mode == MODE_DEV:
self.cognito_client_id = cognito_client_id
Expand All @@ -133,6 +136,7 @@ def __init__(self, hass, mode, alexa, google_actions,
self.relayer = relayer
self.google_actions_sync_url = google_actions_sync_url
self.subscription_info_url = subscription_info_url
self.cloudhook_create_url = cloudhook_create_url

else:
info = SERVERS[mode]
Expand All @@ -143,6 +147,7 @@ def __init__(self, hass, mode, alexa, google_actions,
self.relayer = info['relayer']
self.google_actions_sync_url = info['google_actions_sync_url']
self.subscription_info_url = info['subscription_info_url']
self.cloudhook_create_url = info['cloudhook_create_url']

@property
def is_logged_in(self):
Expand Down
25 changes: 25 additions & 0 deletions homeassistant/components/cloud/cloud_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Cloud APIs."""
from functools import wraps

from . import auth_api


def _check_token(func):
"""Decorate a function to verify valid token."""
@wraps(func)
async def check_token(cloud, *args):
"""Validate token, then call func."""
await cloud.hass.async_add_executor_job(auth_api.check_token, cloud)
return await func(cloud, *args)

return check_token


@_check_token
async def async_create_cloudhook(cloud):
"""Create a cloudhook."""
websession = cloud.hass.helpers.aiohttp_client.async_get_clientsession()
return await websession.post(
cloud.cloudhook_create_url, headers={
'authorization': cloud.id_token
})
66 changes: 66 additions & 0 deletions homeassistant/components/cloud/cloudhooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Manage cloud cloudhooks."""
import async_timeout

from . import cloud_api


class Cloudhooks:
"""Class to help manage cloudhooks."""

def __init__(self, cloud):
"""Initialize cloudhooks."""
self.cloud = cloud
self.cloud.iot.register_on_connect(self.async_publish_cloudhooks)

async def async_publish_cloudhooks(self):
"""Inform the Relayer of the cloudhooks that we support."""
cloudhooks = self.cloud.prefs.cloudhooks
await self.cloud.iot.async_send_message('webhook-register', {
'cloudhook_ids': [info['cloudhook_id'] for info
in cloudhooks.values()]
})

async def async_create(self, webhook_id):
"""Create a cloud webhook."""
cloudhooks = self.cloud.prefs.cloudhooks

if webhook_id in cloudhooks:
raise ValueError('Hook is already enabled for the cloud.')

if not self.cloud.iot.connected:
raise ValueError("Cloud is not connected")

# Create cloud hook
with async_timeout.timeout(10):
resp = await cloud_api.async_create_cloudhook(self.cloud)

data = await resp.json()
cloudhook_id = data['cloudhook_id']
cloudhook_url = data['url']

# Store hook
cloudhooks = dict(cloudhooks)
hook = cloudhooks[webhook_id] = {
'webhook_id': webhook_id,
'cloudhook_id': cloudhook_id,
'cloudhook_url': cloudhook_url
}
await self.cloud.prefs.async_update(cloudhooks=cloudhooks)

await self.async_publish_cloudhooks()

return hook

async def async_delete(self, webhook_id):
"""Delete a cloud webhook."""
cloudhooks = self.cloud.prefs.cloudhooks

if webhook_id not in cloudhooks:
raise ValueError('Hook is not enabled for the cloud.')

# Remove hook
cloudhooks = dict(cloudhooks)
cloudhooks.pop(webhook_id)
await self.cloud.prefs.async_update(cloudhooks=cloudhooks)

await self.async_publish_cloudhooks()
4 changes: 3 additions & 1 deletion homeassistant/components/cloud/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
PREF_ENABLE_ALEXA = 'alexa_enabled'
PREF_ENABLE_GOOGLE = 'google_enabled'
PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock'
PREF_CLOUDHOOKS = 'cloudhooks'

SERVERS = {
'production': {
Expand All @@ -16,7 +17,8 @@
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
'amazonaws.com/prod/smart_home_sync'),
'subscription_info_url': ('https://stripe-api.nabucasa.com/payments/'
'subscription_info')
'subscription_info'),
'cloudhook_create_url': 'https://webhook-api.nabucasa.com/generate'
}
}

Expand Down
98 changes: 83 additions & 15 deletions homeassistant/components/cloud/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from functools import wraps
import logging

import aiohttp
import async_timeout
import voluptuous as vol

Expand Down Expand Up @@ -44,6 +45,20 @@
})


WS_TYPE_HOOK_CREATE = 'cloud/cloudhook/create'
SCHEMA_WS_HOOK_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_HOOK_CREATE,
vol.Required('webhook_id'): str
})


WS_TYPE_HOOK_DELETE = 'cloud/cloudhook/delete'
SCHEMA_WS_HOOK_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_HOOK_DELETE,
vol.Required('webhook_id'): str
})


async def async_setup(hass):
"""Initialize the HTTP API."""
hass.components.websocket_api.async_register_command(
Expand All @@ -58,6 +73,14 @@ async def async_setup(hass):
WS_TYPE_UPDATE_PREFS, websocket_update_prefs,
SCHEMA_WS_UPDATE_PREFS
)
hass.components.websocket_api.async_register_command(
WS_TYPE_HOOK_CREATE, websocket_hook_create,
SCHEMA_WS_HOOK_CREATE
)
hass.components.websocket_api.async_register_command(
WS_TYPE_HOOK_DELETE, websocket_hook_delete,
SCHEMA_WS_HOOK_DELETE
)
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
Expand All @@ -76,7 +99,7 @@ async def async_setup(hass):


def _handle_cloud_errors(handler):
"""Handle auth errors."""
"""Webview decorator to handle auth errors."""
@wraps(handler)
async def error_handler(view, request, *args, **kwargs):
"""Handle exceptions that raise from the wrapped request handler."""
Expand Down Expand Up @@ -240,17 +263,49 @@ def websocket_cloud_status(hass, connection, msg):
websocket_api.result_message(msg['id'], _account_data(cloud)))


def _require_cloud_login(handler):
"""Websocket decorator that requires cloud to be logged in."""
@wraps(handler)
def with_cloud_auth(hass, connection, msg):
"""Require to be logged into the cloud."""
cloud = hass.data[DOMAIN]
if not cloud.is_logged_in:
connection.send_message(websocket_api.error_message(
msg['id'], 'not_logged_in',
'You need to be logged in to the cloud.'))
return

handler(hass, connection, msg)

return with_cloud_auth


def _handle_aiohttp_errors(handler):
"""Websocket decorator that handlers aiohttp errors.

Can only wrap async handlers.
"""
@wraps(handler)
async def with_error_handling(hass, connection, msg):
"""Handle aiohttp errors."""
try:
await handler(hass, connection, msg)
except asyncio.TimeoutError:
connection.send_message(websocket_api.error_message(
msg['id'], 'timeout', 'Command timed out.'))
except aiohttp.ClientError:
connection.send_message(websocket_api.error_message(
msg['id'], 'unknown', 'Error making request.'))

return with_error_handling


@_require_cloud_login
@websocket_api.async_response
async def websocket_subscription(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]

if not cloud.is_logged_in:
connection.send_message(websocket_api.error_message(
msg['id'], 'not_logged_in',
'You need to be logged in to the cloud.'))
return

with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
response = await cloud.fetch_subscription_info()

Expand All @@ -277,24 +332,37 @@ async def websocket_subscription(hass, connection, msg):
connection.send_message(websocket_api.result_message(msg['id'], data))


@_require_cloud_login
@websocket_api.async_response
async def websocket_update_prefs(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]

if not cloud.is_logged_in:
connection.send_message(websocket_api.error_message(
msg['id'], 'not_logged_in',
'You need to be logged in to the cloud.'))
return

changes = dict(msg)
changes.pop('id')
changes.pop('type')
await cloud.prefs.async_update(**changes)

connection.send_message(websocket_api.result_message(
msg['id'], {'success': True}))
connection.send_message(websocket_api.result_message(msg['id']))


@_require_cloud_login
@websocket_api.async_response
@_handle_aiohttp_errors
async def websocket_hook_create(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]
hook = await cloud.cloudhooks.async_create(msg['webhook_id'])
connection.send_message(websocket_api.result_message(msg['id'], hook))


@_require_cloud_login
@websocket_api.async_response
async def websocket_hook_delete(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]
await cloud.cloudhooks.async_delete(msg['webhook_id'])
connection.send_message(websocket_api.result_message(msg['id']))


def _account_data(cloud):
Expand Down
Loading