2017-08-29 20:40:08 +00:00
|
|
|
"""The HTTP api to control the cloud integration."""
|
|
|
|
import asyncio
|
2017-09-12 16:47:04 +00:00
|
|
|
from functools import wraps
|
2017-08-29 20:40:08 +00:00
|
|
|
import logging
|
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
import async_timeout
|
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
from homeassistant.components.http import (
|
|
|
|
HomeAssistantView, RequestDataValidator)
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
from . import auth_api
|
2017-10-15 02:43:14 +00:00
|
|
|
from .const import DOMAIN, REQUEST_TIMEOUT
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_setup(hass):
|
|
|
|
"""Initialize the HTTP api."""
|
|
|
|
hass.http.register_view(CloudLoginView)
|
|
|
|
hass.http.register_view(CloudLogoutView)
|
|
|
|
hass.http.register_view(CloudAccountView)
|
2017-09-12 16:47:04 +00:00
|
|
|
hass.http.register_view(CloudRegisterView)
|
|
|
|
hass.http.register_view(CloudConfirmRegisterView)
|
|
|
|
hass.http.register_view(CloudForgotPasswordView)
|
|
|
|
hass.http.register_view(CloudConfirmForgotPasswordView)
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
_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.'),
|
|
|
|
auth_api.ExpiredCode: (400, 'Confirmation code has expired.'),
|
|
|
|
auth_api.InvalidCode: (400, 'Invalid confirmation code.'),
|
|
|
|
asyncio.TimeoutError: (502, 'Unable to reach the Home Assistant cloud.')
|
|
|
|
}
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
def _handle_cloud_errors(handler):
|
|
|
|
"""Helper method to handle auth errors."""
|
2017-08-29 20:40:08 +00:00
|
|
|
@asyncio.coroutine
|
2017-09-12 16:47:04 +00:00
|
|
|
@wraps(handler)
|
|
|
|
def error_handler(view, request, *args, **kwargs):
|
|
|
|
"""Handle exceptions that raise from the wrapped request handler."""
|
2017-08-29 20:40:08 +00:00
|
|
|
try:
|
2017-09-12 16:47:04 +00:00
|
|
|
result = yield from handler(view, request, *args, **kwargs)
|
|
|
|
return result
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
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__)
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
return error_handler
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
class CloudLoginView(HomeAssistantView):
|
|
|
|
"""Login to Home Assistant cloud."""
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
url = '/api/cloud/login'
|
|
|
|
name = 'api:cloud:login'
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
@_handle_cloud_errors
|
|
|
|
@RequestDataValidator(vol.Schema({
|
|
|
|
vol.Required('email'): str,
|
|
|
|
vol.Required('password'): str,
|
|
|
|
}))
|
2017-11-21 05:44:22 +00:00
|
|
|
@asyncio.coroutine
|
2017-09-12 16:47:04 +00:00
|
|
|
def post(self, request, data):
|
|
|
|
"""Handle login request."""
|
|
|
|
hass = request.app['hass']
|
2017-10-15 02:43:14 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
2017-10-15 02:43:14 +00:00
|
|
|
yield from hass.async_add_job(auth_api.login, cloud, data['email'],
|
2017-09-12 16:47:04 +00:00
|
|
|
data['password'])
|
|
|
|
|
2017-11-15 07:16:19 +00:00
|
|
|
hass.async_add_job(cloud.iot.connect)
|
|
|
|
# Allow cloud to start connecting.
|
|
|
|
yield from asyncio.sleep(0, loop=hass.loop)
|
2017-10-15 02:43:14 +00:00
|
|
|
return self.json(_account_data(cloud))
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
class CloudLogoutView(HomeAssistantView):
|
|
|
|
"""Log out of the Home Assistant cloud."""
|
|
|
|
|
|
|
|
url = '/api/cloud/logout'
|
|
|
|
name = 'api:cloud:logout'
|
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
@_handle_cloud_errors
|
2017-11-21 05:44:22 +00:00
|
|
|
@asyncio.coroutine
|
2017-08-29 20:40:08 +00:00
|
|
|
def post(self, request):
|
2017-09-12 16:47:04 +00:00
|
|
|
"""Handle logout request."""
|
2017-08-29 20:40:08 +00:00
|
|
|
hass = request.app['hass']
|
2017-10-15 02:43:14 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
2017-10-15 02:43:14 +00:00
|
|
|
yield from cloud.logout()
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
return self.json_message('ok')
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
class CloudAccountView(HomeAssistantView):
|
2017-09-12 16:47:04 +00:00
|
|
|
"""View to retrieve account info."""
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
url = '/api/cloud/account'
|
|
|
|
name = 'api:cloud:account'
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def get(self, request):
|
2017-09-12 16:47:04 +00:00
|
|
|
"""Get account info."""
|
2017-08-29 20:40:08 +00:00
|
|
|
hass = request.app['hass']
|
2017-10-15 02:43:14 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-10-15 02:43:14 +00:00
|
|
|
if not cloud.is_logged_in:
|
2017-08-29 20:40:08 +00:00
|
|
|
return self.json_message('Not logged in', 400)
|
|
|
|
|
2017-10-15 02:43:14 +00:00
|
|
|
return self.json(_account_data(cloud))
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
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)),
|
|
|
|
}))
|
2017-11-21 05:44:22 +00:00
|
|
|
@asyncio.coroutine
|
2017-09-12 16:47:04 +00:00
|
|
|
def post(self, request, data):
|
|
|
|
"""Handle registration request."""
|
|
|
|
hass = request.app['hass']
|
2017-10-15 02:43:14 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
|
|
|
yield from hass.async_add_job(
|
2017-10-15 02:43:14 +00:00
|
|
|
auth_api.register, cloud, data['email'], data['password'])
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
return self.json_message('ok')
|
|
|
|
|
|
|
|
|
|
|
|
class CloudConfirmRegisterView(HomeAssistantView):
|
|
|
|
"""Confirm registration on the Home Assistant cloud."""
|
|
|
|
|
|
|
|
url = '/api/cloud/confirm_register'
|
|
|
|
name = 'api:cloud:confirm_register'
|
|
|
|
|
|
|
|
@_handle_cloud_errors
|
|
|
|
@RequestDataValidator(vol.Schema({
|
|
|
|
vol.Required('confirmation_code'): str,
|
|
|
|
vol.Required('email'): str,
|
|
|
|
}))
|
2017-11-21 05:44:22 +00:00
|
|
|
@asyncio.coroutine
|
2017-09-12 16:47:04 +00:00
|
|
|
def post(self, request, data):
|
|
|
|
"""Handle registration confirmation request."""
|
|
|
|
hass = request.app['hass']
|
2017-10-15 02:43:14 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
|
|
|
yield from hass.async_add_job(
|
2017-10-15 02:43:14 +00:00
|
|
|
auth_api.confirm_register, cloud, data['confirmation_code'],
|
2017-09-12 16:47:04 +00:00
|
|
|
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,
|
|
|
|
}))
|
2017-11-21 05:44:22 +00:00
|
|
|
@asyncio.coroutine
|
2017-09-12 16:47:04 +00:00
|
|
|
def post(self, request, data):
|
|
|
|
"""Handle forgot password request."""
|
|
|
|
hass = request.app['hass']
|
2017-10-15 02:43:14 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
|
|
|
yield from hass.async_add_job(
|
2017-10-15 02:43:14 +00:00
|
|
|
auth_api.forgot_password, cloud, data['email'])
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
return self.json_message('ok')
|
|
|
|
|
|
|
|
|
|
|
|
class CloudConfirmForgotPasswordView(HomeAssistantView):
|
|
|
|
"""View to finish Forgot Password flow.."""
|
|
|
|
|
|
|
|
url = '/api/cloud/confirm_forgot_password'
|
|
|
|
name = 'api:cloud:confirm_forgot_password'
|
|
|
|
|
|
|
|
@_handle_cloud_errors
|
|
|
|
@RequestDataValidator(vol.Schema({
|
|
|
|
vol.Required('confirmation_code'): str,
|
|
|
|
vol.Required('email'): str,
|
|
|
|
vol.Required('new_password'): vol.All(str, vol.Length(min=6))
|
|
|
|
}))
|
2017-11-21 05:44:22 +00:00
|
|
|
@asyncio.coroutine
|
2017-09-12 16:47:04 +00:00
|
|
|
def post(self, request, data):
|
|
|
|
"""Handle forgot password confirm request."""
|
|
|
|
hass = request.app['hass']
|
2017-10-15 02:43:14 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
|
|
|
yield from hass.async_add_job(
|
2017-10-15 02:43:14 +00:00
|
|
|
auth_api.confirm_forgot_password, cloud,
|
2017-09-12 16:47:04 +00:00
|
|
|
data['confirmation_code'], data['email'],
|
|
|
|
data['new_password'])
|
|
|
|
|
|
|
|
return self.json_message('ok')
|
|
|
|
|
|
|
|
|
2017-10-15 02:43:14 +00:00
|
|
|
def _account_data(cloud):
|
2017-09-12 16:47:04 +00:00
|
|
|
"""Generate the auth data JSON response."""
|
2017-11-15 07:16:19 +00:00
|
|
|
claims = cloud.claims
|
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
return {
|
2017-11-15 07:16:19 +00:00
|
|
|
'email': claims['email'],
|
|
|
|
'sub_exp': claims.get('custom:sub-exp'),
|
|
|
|
'cloud': cloud.iot.state,
|
2017-09-12 16:47:04 +00:00
|
|
|
}
|