core/homeassistant/components/cloud/http_api.py

263 lines
7.9 KiB
Python
Raw Normal View History

"""The HTTP api to control the cloud integration."""
import asyncio
from functools import wraps
import logging
import async_timeout
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import (
RequestDataValidator)
from homeassistant.components import websocket_api
from . import auth_api
from .const import DOMAIN, REQUEST_TIMEOUT
from .iot import STATE_DISCONNECTED
_LOGGER = logging.getLogger(__name__)
WS_TYPE_STATUS = 'cloud/status'
SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_STATUS,
})
WS_TYPE_SUBSCRIPTION = 'cloud/subscription'
SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SUBSCRIPTION,
})
async def async_setup(hass):
"""Initialize the HTTP API."""
hass.components.websocket_api.async_register_command(
WS_TYPE_STATUS, websocket_cloud_status,
SCHEMA_WS_STATUS
)
hass.components.websocket_api.async_register_command(
WS_TYPE_SUBSCRIPTION, websocket_subscription,
SCHEMA_WS_SUBSCRIPTION
)
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
hass.http.register_view(CloudRegisterView)
hass.http.register_view(CloudResendConfirmView)
hass.http.register_view(CloudForgotPasswordView)
_CLOUD_ERRORS = {
auth_api.UserNotFound: (400, "User does not exist."),
auth_api.UserNotConfirmed: (400, 'Email not confirmed.'),
auth_api.Unauthenticated: (401, 'Authentication failed.'),
auth_api.PasswordChangeRequired: (400, 'Password change required.'),
asyncio.TimeoutError: (502, 'Unable to reach the Home Assistant cloud.')
}
def _handle_cloud_errors(handler):
"""Handle auth errors."""
@wraps(handler)
async def error_handler(view, request, *args, **kwargs):
"""Handle exceptions that raise from the wrapped request handler."""
try:
result = await handler(view, request, *args, **kwargs)
return result
except (auth_api.CloudError, asyncio.TimeoutError) as err:
err_info = _CLOUD_ERRORS.get(err.__class__)
if err_info is None:
err_info = (502, 'Unexpected error: {}'.format(err))
status, msg = err_info
return view.json_message(msg, status_code=status,
message_code=err.__class__.__name__)
return error_handler
class GoogleActionsSyncView(HomeAssistantView):
"""Trigger a Google Actions Smart Home Sync."""
url = '/api/cloud/google_actions/sync'
name = 'api:cloud:google_actions/sync'
@_handle_cloud_errors
async def post(self, request):
"""Trigger a Google Actions sync."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
websession = hass.helpers.aiohttp_client.async_get_clientsession()
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(auth_api.check_token, cloud)
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
req = await websession.post(
cloud.google_actions_sync_url, headers={
'authorization': cloud.id_token
})
return self.json({}, status_code=req.status)
class CloudLoginView(HomeAssistantView):
"""Login to Home Assistant cloud."""
url = '/api/cloud/login'
name = 'api:cloud:login'
@_handle_cloud_errors
@RequestDataValidator(vol.Schema({
vol.Required('email'): str,
vol.Required('password'): str,
}))
async def post(self, request, data):
"""Handle login request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(auth_api.login, cloud, data['email'],
data['password'])
hass.async_add_job(cloud.iot.connect)
return self.json({'success': True})
class CloudLogoutView(HomeAssistantView):
"""Log out of the Home Assistant cloud."""
url = '/api/cloud/logout'
name = 'api:cloud:logout'
@_handle_cloud_errors
async def post(self, request):
"""Handle logout request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await cloud.logout()
return self.json_message('ok')
class CloudRegisterView(HomeAssistantView):
"""Register on the Home Assistant cloud."""
url = '/api/cloud/register'
name = 'api:cloud:register'
@_handle_cloud_errors
@RequestDataValidator(vol.Schema({
vol.Required('email'): str,
vol.Required('password'): vol.All(str, vol.Length(min=6)),
}))
async def post(self, request, data):
"""Handle registration request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(
auth_api.register, cloud, data['email'], data['password'])
return self.json_message('ok')
class CloudResendConfirmView(HomeAssistantView):
"""Resend email confirmation code."""
url = '/api/cloud/resend_confirm'
name = 'api:cloud:resend_confirm'
@_handle_cloud_errors
@RequestDataValidator(vol.Schema({
vol.Required('email'): str,
}))
async def post(self, request, data):
"""Handle resending confirm email code request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(
auth_api.resend_email_confirm, cloud, data['email'])
return self.json_message('ok')
class CloudForgotPasswordView(HomeAssistantView):
"""View to start Forgot Password flow.."""
url = '/api/cloud/forgot_password'
name = 'api:cloud:forgot_password'
@_handle_cloud_errors
@RequestDataValidator(vol.Schema({
vol.Required('email'): str,
}))
async def post(self, request, data):
"""Handle forgot password request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(
auth_api.forgot_password, cloud, data['email'])
return self.json_message('ok')
@callback
def websocket_cloud_status(hass, connection, msg):
"""Handle request for account info.
Async friendly.
"""
cloud = hass.data[DOMAIN]
connection.to_write.put_nowait(
websocket_api.result_message(msg['id'], _account_data(cloud)))
@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.to_write.put_nowait(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()
if response.status == 200:
connection.send_message_outside(websocket_api.result_message(
msg['id'], await response.json()))
else:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'request_failed', 'Failed to request subscription'))
def _account_data(cloud):
"""Generate the auth data JSON response."""
if not cloud.is_logged_in:
return {
'logged_in': False,
'cloud': STATE_DISCONNECTED,
}
claims = cloud.claims
return {
'logged_in': True,
'email': claims['email'],
'cloud': cloud.iot.state,
}