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
|
|
|
|
|
2018-11-26 13:10:18 +00:00
|
|
|
import aiohttp
|
2017-08-29 20:40:08 +00:00
|
|
|
import async_timeout
|
2019-10-18 22:39:37 +00:00
|
|
|
import attr
|
2019-11-08 09:06:16 +00:00
|
|
|
from hass_nabucasa import Cloud, auth, thingtalk
|
2019-10-18 22:39:37 +00:00
|
|
|
from hass_nabucasa.const import STATE_DISCONNECTED
|
2018-01-21 06:35:38 +00:00
|
|
|
import voluptuous as vol
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2018-09-20 12:53:13 +00:00
|
|
|
from homeassistant.components import websocket_api
|
2019-06-25 05:04:31 +00:00
|
|
|
from homeassistant.components.alexa import (
|
|
|
|
entities as alexa_entities,
|
|
|
|
errors as alexa_errors,
|
|
|
|
)
|
2019-05-29 15:39:12 +00:00
|
|
|
from homeassistant.components.google_assistant import helpers as google_helpers
|
2019-10-18 22:39:37 +00:00
|
|
|
from homeassistant.components.http import HomeAssistantView
|
|
|
|
from homeassistant.components.http.data_validator import RequestDataValidator
|
|
|
|
from homeassistant.components.websocket_api import const as ws_const
|
2020-04-09 19:43:42 +00:00
|
|
|
from homeassistant.const import HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_OK
|
2019-10-18 22:39:37 +00:00
|
|
|
from homeassistant.core import callback
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2018-11-20 22:23:07 +00:00
|
|
|
from .const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN,
|
2019-10-18 22:39:37 +00:00
|
|
|
PREF_ALEXA_REPORT_STATE,
|
2019-07-31 19:25:30 +00:00
|
|
|
PREF_ENABLE_ALEXA,
|
|
|
|
PREF_ENABLE_GOOGLE,
|
2019-10-18 22:39:37 +00:00
|
|
|
PREF_GOOGLE_REPORT_STATE,
|
2019-07-31 19:25:30 +00:00
|
|
|
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
2019-10-18 22:39:37 +00:00
|
|
|
REQUEST_TIMEOUT,
|
2019-07-31 19:25:30 +00:00
|
|
|
InvalidTrustedNetworks,
|
|
|
|
InvalidTrustedProxies,
|
|
|
|
RequireRelink,
|
|
|
|
)
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
WS_TYPE_STATUS = "cloud/status"
|
|
|
|
SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
|
|
|
{vol.Required("type"): WS_TYPE_STATUS}
|
|
|
|
)
|
2018-09-20 12:53:13 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
WS_TYPE_SUBSCRIPTION = "cloud/subscription"
|
|
|
|
SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
|
|
|
{vol.Required("type"): WS_TYPE_SUBSCRIPTION}
|
|
|
|
)
|
2018-09-20 12:53:13 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
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}
|
|
|
|
)
|
2018-11-26 13:10:18 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
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}
|
|
|
|
)
|
2018-11-26 13:10:18 +00:00
|
|
|
|
|
|
|
|
2019-03-16 02:26:10 +00:00
|
|
|
_CLOUD_ERRORS = {
|
2019-07-31 19:25:30 +00:00
|
|
|
InvalidTrustedNetworks: (
|
2020-04-08 21:20:03 +00:00
|
|
|
HTTP_INTERNAL_SERVER_ERROR,
|
2019-07-31 19:46:17 +00:00
|
|
|
"Remote UI not compatible with 127.0.0.1/::1 as a trusted network.",
|
2019-07-31 19:25:30 +00:00
|
|
|
),
|
|
|
|
InvalidTrustedProxies: (
|
2020-04-08 21:20:03 +00:00
|
|
|
HTTP_INTERNAL_SERVER_ERROR,
|
2019-07-31 19:46:17 +00:00
|
|
|
"Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.",
|
2019-07-31 19:25:30 +00:00
|
|
|
),
|
2020-06-12 05:56:00 +00:00
|
|
|
asyncio.TimeoutError: (502, "Unable to reach the Home Assistant cloud."),
|
|
|
|
aiohttp.ClientError: (HTTP_INTERNAL_SERVER_ERROR, "Error making internal request",),
|
2019-03-16 02:26:10 +00:00
|
|
|
}
|
2019-03-11 19:21:20 +00:00
|
|
|
|
|
|
|
|
2018-03-23 19:13:52 +00:00
|
|
|
async def async_setup(hass):
|
2018-01-21 06:35:38 +00:00
|
|
|
"""Initialize the HTTP API."""
|
2019-11-08 09:06:16 +00:00
|
|
|
async_register_command = hass.components.websocket_api.async_register_command
|
|
|
|
async_register_command(WS_TYPE_STATUS, websocket_cloud_status, SCHEMA_WS_STATUS)
|
|
|
|
async_register_command(
|
2019-07-31 19:25:30 +00:00
|
|
|
WS_TYPE_SUBSCRIPTION, websocket_subscription, SCHEMA_WS_SUBSCRIPTION
|
2018-09-20 12:53:13 +00:00
|
|
|
)
|
2019-11-08 09:06:16 +00:00
|
|
|
async_register_command(websocket_update_prefs)
|
|
|
|
async_register_command(
|
2019-07-31 19:25:30 +00:00
|
|
|
WS_TYPE_HOOK_CREATE, websocket_hook_create, SCHEMA_WS_HOOK_CREATE
|
2018-11-26 13:10:18 +00:00
|
|
|
)
|
2019-11-08 09:06:16 +00:00
|
|
|
async_register_command(
|
2019-07-31 19:25:30 +00:00
|
|
|
WS_TYPE_HOOK_DELETE, websocket_hook_delete, SCHEMA_WS_HOOK_DELETE
|
2018-11-26 13:10:18 +00:00
|
|
|
)
|
2019-11-08 09:06:16 +00:00
|
|
|
async_register_command(websocket_remote_connect)
|
|
|
|
async_register_command(websocket_remote_disconnect)
|
|
|
|
|
|
|
|
async_register_command(google_assistant_list)
|
|
|
|
async_register_command(google_assistant_update)
|
2019-05-29 15:39:12 +00:00
|
|
|
|
2019-11-08 09:06:16 +00:00
|
|
|
async_register_command(alexa_list)
|
|
|
|
async_register_command(alexa_update)
|
|
|
|
async_register_command(alexa_sync)
|
2019-05-29 15:39:12 +00:00
|
|
|
|
2019-11-08 09:06:16 +00:00
|
|
|
async_register_command(thingtalk_convert)
|
2019-06-13 18:58:08 +00:00
|
|
|
|
2018-03-23 19:13:52 +00:00
|
|
|
hass.http.register_view(GoogleActionsSyncView)
|
2017-08-29 20:40:08 +00:00
|
|
|
hass.http.register_view(CloudLoginView)
|
|
|
|
hass.http.register_view(CloudLogoutView)
|
2017-09-12 16:47:04 +00:00
|
|
|
hass.http.register_view(CloudRegisterView)
|
2017-12-29 13:46:10 +00:00
|
|
|
hass.http.register_view(CloudResendConfirmView)
|
2017-09-12 16:47:04 +00:00
|
|
|
hass.http.register_view(CloudForgotPasswordView)
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
_CLOUD_ERRORS.update(
|
|
|
|
{
|
2020-04-09 19:43:42 +00:00
|
|
|
auth.UserNotFound: (HTTP_BAD_REQUEST, "User does not exist."),
|
|
|
|
auth.UserNotConfirmed: (HTTP_BAD_REQUEST, "Email not confirmed."),
|
|
|
|
auth.UserExists: (
|
|
|
|
HTTP_BAD_REQUEST,
|
|
|
|
"An account with the given email already exists.",
|
|
|
|
),
|
2019-07-31 19:25:30 +00:00
|
|
|
auth.Unauthenticated: (401, "Authentication failed."),
|
2020-04-09 19:43:42 +00:00
|
|
|
auth.PasswordChangeRequired: (
|
|
|
|
HTTP_BAD_REQUEST,
|
|
|
|
"Password change required.",
|
|
|
|
),
|
2019-07-31 19:25:30 +00:00
|
|
|
}
|
|
|
|
)
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
def _handle_cloud_errors(handler):
|
2018-11-26 13:10:18 +00:00
|
|
|
"""Webview decorator to handle auth errors."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
@wraps(handler)
|
2018-03-23 19:13:52 +00:00
|
|
|
async def error_handler(view, request, *args, **kwargs):
|
2017-09-12 16:47:04 +00:00
|
|
|
"""Handle exceptions that raise from the wrapped request handler."""
|
2017-08-29 20:40:08 +00:00
|
|
|
try:
|
2018-03-23 19:13:52 +00:00
|
|
|
result = await handler(view, request, *args, **kwargs)
|
2017-09-12 16:47:04 +00:00
|
|
|
return result
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2019-02-04 09:14:30 +00:00
|
|
|
except Exception as err: # pylint: disable=broad-except
|
2019-03-16 02:26:10 +00:00
|
|
|
status, msg = _process_cloud_exception(err, request.path)
|
2019-02-04 09:14:30 +00:00
|
|
|
return view.json_message(
|
2019-07-31 19:25:30 +00:00
|
|
|
msg, status_code=status, message_code=err.__class__.__name__.lower()
|
|
|
|
)
|
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
|
|
|
|
|
|
|
|
2019-03-16 02:26:10 +00:00
|
|
|
def _ws_handle_cloud_errors(handler):
|
|
|
|
"""Websocket decorator to handle auth errors."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2019-03-16 02:26:10 +00:00
|
|
|
@wraps(handler)
|
|
|
|
async def error_handler(hass, connection, msg):
|
|
|
|
"""Handle exceptions that raise from the wrapped handler."""
|
|
|
|
try:
|
|
|
|
return await handler(hass, connection, msg)
|
|
|
|
|
|
|
|
except Exception as err: # pylint: disable=broad-except
|
2019-07-31 19:25:30 +00:00
|
|
|
err_status, err_msg = _process_cloud_exception(err, msg["type"])
|
|
|
|
connection.send_error(msg["id"], err_status, err_msg)
|
2019-03-16 02:26:10 +00:00
|
|
|
|
|
|
|
return error_handler
|
|
|
|
|
|
|
|
|
|
|
|
def _process_cloud_exception(exc, where):
|
|
|
|
"""Process a cloud exception."""
|
2020-06-12 05:56:00 +00:00
|
|
|
err_info = None
|
|
|
|
|
|
|
|
for err, value_info in _CLOUD_ERRORS.items():
|
|
|
|
if isinstance(exc, err):
|
|
|
|
err_info = value_info
|
|
|
|
break
|
|
|
|
|
2019-03-16 02:26:10 +00:00
|
|
|
if err_info is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.exception("Unexpected error processing request for %s", where)
|
2019-09-03 15:09:59 +00:00
|
|
|
err_info = (502, f"Unexpected error: {exc}")
|
2020-06-12 05:56:00 +00:00
|
|
|
|
2019-03-16 02:26:10 +00:00
|
|
|
return err_info
|
|
|
|
|
|
|
|
|
2018-03-23 19:13:52 +00:00
|
|
|
class GoogleActionsSyncView(HomeAssistantView):
|
|
|
|
"""Trigger a Google Actions Smart Home Sync."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/api/cloud/google_actions/sync"
|
|
|
|
name = "api:cloud:google_actions/sync"
|
2018-03-23 19:13:52 +00:00
|
|
|
|
|
|
|
@_handle_cloud_errors
|
|
|
|
async def post(self, request):
|
|
|
|
"""Trigger a Google Actions sync."""
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
2019-10-03 11:02:38 +00:00
|
|
|
cloud: Cloud = hass.data[DOMAIN]
|
2019-11-28 13:23:59 +00:00
|
|
|
gconf = await cloud.client.get_google_config()
|
2020-01-06 21:00:39 +00:00
|
|
|
status = await gconf.async_sync_entities(gconf.agent_user_id)
|
2019-10-03 11:02:38 +00:00
|
|
|
return self.json({}, status_code=status)
|
2018-03-23 19:13:52 +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
|
|
|
|
2019-07-31 19:25:30 +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
|
2019-07-31 19:25:30 +00:00
|
|
|
@RequestDataValidator(
|
|
|
|
vol.Schema({vol.Required("email"): str, vol.Required("password"): str})
|
|
|
|
)
|
2018-03-23 19:13:52 +00:00
|
|
|
async def post(self, request, data):
|
2017-09-12 16:47:04 +00:00
|
|
|
"""Handle login request."""
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
2017-10-15 02:43:14 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
2019-11-28 13:23:59 +00:00
|
|
|
await cloud.login(data["email"], data["password"])
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json({"success": True})
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
class CloudLogoutView(HomeAssistantView):
|
|
|
|
"""Log out of the Home Assistant cloud."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/api/cloud/logout"
|
|
|
|
name = "api:cloud:logout"
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
@_handle_cloud_errors
|
2018-03-23 19:13:52 +00:00
|
|
|
async def post(self, request):
|
2017-09-12 16:47:04 +00:00
|
|
|
"""Handle logout request."""
|
2019-07-31 19:25:30 +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
|
|
|
|
2019-05-23 04:09:59 +00:00
|
|
|
with async_timeout.timeout(REQUEST_TIMEOUT):
|
2018-03-23 19:13:52 +00:00
|
|
|
await cloud.logout()
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("ok")
|
2017-08-29 20:40:08 +00:00
|
|
|
|
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
class CloudRegisterView(HomeAssistantView):
|
|
|
|
"""Register on the Home Assistant cloud."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/api/cloud/register"
|
|
|
|
name = "api:cloud:register"
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
@_handle_cloud_errors
|
2019-07-31 19:25:30 +00:00
|
|
|
@RequestDataValidator(
|
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required("email"): str,
|
|
|
|
vol.Required("password"): vol.All(str, vol.Length(min=6)),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
2018-03-23 19:13:52 +00:00
|
|
|
async def post(self, request, data):
|
2017-09-12 16:47:04 +00:00
|
|
|
"""Handle registration request."""
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
2017-10-15 02:43:14 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
2017-09-12 16:47:04 +00:00
|
|
|
|
2019-05-23 04:09:59 +00:00
|
|
|
with async_timeout.timeout(REQUEST_TIMEOUT):
|
2020-03-10 00:42:26 +00:00
|
|
|
await cloud.auth.async_register(data["email"], data["password"])
|
2017-09-12 16:47:04 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("ok")
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
|
2017-12-29 13:46:10 +00:00
|
|
|
class CloudResendConfirmView(HomeAssistantView):
|
|
|
|
"""Resend email confirmation code."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/api/cloud/resend_confirm"
|
|
|
|
name = "api:cloud:resend_confirm"
|
2017-12-29 13:46:10 +00:00
|
|
|
|
|
|
|
@_handle_cloud_errors
|
2019-07-31 19:25:30 +00:00
|
|
|
@RequestDataValidator(vol.Schema({vol.Required("email"): str}))
|
2018-03-23 19:13:52 +00:00
|
|
|
async def post(self, request, data):
|
2017-12-29 13:46:10 +00:00
|
|
|
"""Handle resending confirm email code request."""
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
2017-12-29 13:46:10 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
|
|
|
|
2019-05-23 04:09:59 +00:00
|
|
|
with async_timeout.timeout(REQUEST_TIMEOUT):
|
2020-03-10 00:42:26 +00:00
|
|
|
await cloud.auth.async_resend_email_confirm(data["email"])
|
2017-12-29 13:46:10 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("ok")
|
2017-12-29 13:46:10 +00:00
|
|
|
|
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
class CloudForgotPasswordView(HomeAssistantView):
|
|
|
|
"""View to start Forgot Password flow.."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/api/cloud/forgot_password"
|
|
|
|
name = "api:cloud:forgot_password"
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
@_handle_cloud_errors
|
2019-07-31 19:25:30 +00:00
|
|
|
@RequestDataValidator(vol.Schema({vol.Required("email"): str}))
|
2018-03-23 19:13:52 +00:00
|
|
|
async def post(self, request, data):
|
2017-09-12 16:47:04 +00:00
|
|
|
"""Handle forgot password request."""
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
2017-10-15 02:43:14 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
2017-09-12 16:47:04 +00:00
|
|
|
|
2019-05-23 04:09:59 +00:00
|
|
|
with async_timeout.timeout(REQUEST_TIMEOUT):
|
2020-03-10 00:42:26 +00:00
|
|
|
await cloud.auth.async_forgot_password(data["email"])
|
2017-09-12 16:47:04 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("ok")
|
2017-09-12 16:47:04 +00:00
|
|
|
|
|
|
|
|
2018-09-20 12:53:13 +00:00
|
|
|
@callback
|
|
|
|
def websocket_cloud_status(hass, connection, msg):
|
|
|
|
"""Handle request for account info.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
|
|
|
cloud = hass.data[DOMAIN]
|
2018-10-01 14:09:31 +00:00
|
|
|
connection.send_message(
|
2019-07-31 19:25:30 +00:00
|
|
|
websocket_api.result_message(msg["id"], _account_data(cloud))
|
|
|
|
)
|
2018-09-20 12:53:13 +00:00
|
|
|
|
|
|
|
|
2018-11-26 13:10:18 +00:00
|
|
|
def _require_cloud_login(handler):
|
|
|
|
"""Websocket decorator that requires cloud to be logged in."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2018-11-26 13:10:18 +00:00
|
|
|
@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:
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_message(
|
|
|
|
websocket_api.error_message(
|
|
|
|
msg["id"], "not_logged_in", "You need to be logged in to the cloud."
|
|
|
|
)
|
|
|
|
)
|
2018-11-26 13:10:18 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
handler(hass, connection, msg)
|
|
|
|
|
|
|
|
return with_cloud_auth
|
|
|
|
|
|
|
|
|
|
|
|
@_require_cloud_login
|
2018-09-20 12:53:13 +00:00
|
|
|
@websocket_api.async_response
|
|
|
|
async def websocket_subscription(hass, connection, msg):
|
|
|
|
"""Handle request for account info."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2018-09-20 12:53:13 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
|
|
|
|
2019-05-23 04:09:59 +00:00
|
|
|
with async_timeout.timeout(REQUEST_TIMEOUT):
|
2018-09-20 12:53:13 +00:00
|
|
|
response = await cloud.fetch_subscription_info()
|
|
|
|
|
2020-04-08 16:47:38 +00:00
|
|
|
if response.status != HTTP_OK:
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_message(
|
|
|
|
websocket_api.error_message(
|
|
|
|
msg["id"], "request_failed", "Failed to request subscription"
|
|
|
|
)
|
|
|
|
)
|
2018-10-21 10:16:24 +00:00
|
|
|
|
|
|
|
data = await response.json()
|
|
|
|
|
|
|
|
# Check if a user is subscribed but local info is outdated
|
|
|
|
# In that case, let's refresh and reconnect
|
2019-07-31 19:25:30 +00:00
|
|
|
if data.get("provider") and not cloud.is_connected:
|
|
|
|
_LOGGER.debug("Found disconnected account with valid subscriotion, connecting")
|
2020-03-10 00:42:26 +00:00
|
|
|
await cloud.auth.async_renew_access_token()
|
2018-10-21 10:16:24 +00:00
|
|
|
|
|
|
|
# Cancel reconnect in progress
|
|
|
|
if cloud.iot.state != STATE_DISCONNECTED:
|
|
|
|
await cloud.iot.disconnect()
|
|
|
|
|
|
|
|
hass.async_create_task(cloud.iot.connect())
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_message(websocket_api.result_message(msg["id"], data))
|
2018-09-20 12:53:13 +00:00
|
|
|
|
|
|
|
|
2018-11-26 13:10:18 +00:00
|
|
|
@_require_cloud_login
|
2018-09-20 21:46:51 +00:00
|
|
|
@websocket_api.async_response
|
2019-07-31 19:25:30 +00:00
|
|
|
@websocket_api.websocket_command(
|
|
|
|
{
|
|
|
|
vol.Required("type"): "cloud/update_prefs",
|
|
|
|
vol.Optional(PREF_ENABLE_GOOGLE): bool,
|
|
|
|
vol.Optional(PREF_ENABLE_ALEXA): bool,
|
|
|
|
vol.Optional(PREF_ALEXA_REPORT_STATE): bool,
|
2019-10-03 11:02:38 +00:00
|
|
|
vol.Optional(PREF_GOOGLE_REPORT_STATE): bool,
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
|
|
|
|
}
|
|
|
|
)
|
2018-09-20 21:46:51 +00:00
|
|
|
async def websocket_update_prefs(hass, connection, msg):
|
|
|
|
"""Handle request for account info."""
|
|
|
|
cloud = hass.data[DOMAIN]
|
|
|
|
|
|
|
|
changes = dict(msg)
|
2019-07-31 19:25:30 +00:00
|
|
|
changes.pop("id")
|
|
|
|
changes.pop("type")
|
2019-06-25 05:04:31 +00:00
|
|
|
|
|
|
|
# If we turn alexa linking on, validate that we can fetch access token
|
|
|
|
if changes.get(PREF_ALEXA_REPORT_STATE):
|
|
|
|
try:
|
|
|
|
with async_timeout.timeout(10):
|
|
|
|
await cloud.client.alexa_config.async_get_access_token()
|
|
|
|
except asyncio.TimeoutError:
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_error(
|
|
|
|
msg["id"], "alexa_timeout", "Timeout validating Alexa access token."
|
|
|
|
)
|
2019-06-25 05:04:31 +00:00
|
|
|
return
|
2019-06-27 03:24:20 +00:00
|
|
|
except (alexa_errors.NoTokenAvailable, RequireRelink):
|
2019-06-25 05:04:31 +00:00
|
|
|
connection.send_error(
|
2019-07-31 19:25:30 +00:00
|
|
|
msg["id"],
|
|
|
|
"alexa_relink",
|
|
|
|
"Please go to the Alexa app and re-link the Home Assistant "
|
|
|
|
"skill and then try to enable state reporting.",
|
2019-06-25 05:04:31 +00:00
|
|
|
)
|
|
|
|
return
|
|
|
|
|
2019-03-11 19:21:20 +00:00
|
|
|
await cloud.client.prefs.async_update(**changes)
|
2018-09-20 21:46:51 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_message(websocket_api.result_message(msg["id"]))
|
2018-11-26 13:10:18 +00:00
|
|
|
|
|
|
|
|
|
|
|
@_require_cloud_login
|
|
|
|
@websocket_api.async_response
|
2019-03-16 02:26:10 +00:00
|
|
|
@_ws_handle_cloud_errors
|
2018-11-26 13:10:18 +00:00
|
|
|
async def websocket_hook_create(hass, connection, msg):
|
|
|
|
"""Handle request for account info."""
|
|
|
|
cloud = hass.data[DOMAIN]
|
2019-07-31 19:25:30 +00:00
|
|
|
hook = await cloud.cloudhooks.async_create(msg["webhook_id"], False)
|
|
|
|
connection.send_message(websocket_api.result_message(msg["id"], hook))
|
2018-11-26 13:10:18 +00:00
|
|
|
|
|
|
|
|
|
|
|
@_require_cloud_login
|
|
|
|
@websocket_api.async_response
|
2019-03-16 02:26:10 +00:00
|
|
|
@_ws_handle_cloud_errors
|
2018-11-26 13:10:18 +00:00
|
|
|
async def websocket_hook_delete(hass, connection, msg):
|
|
|
|
"""Handle request for account info."""
|
|
|
|
cloud = hass.data[DOMAIN]
|
2019-07-31 19:25:30 +00:00
|
|
|
await cloud.cloudhooks.async_delete(msg["webhook_id"])
|
|
|
|
connection.send_message(websocket_api.result_message(msg["id"]))
|
2018-09-20 21:46:51 +00:00
|
|
|
|
|
|
|
|
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."""
|
2019-03-11 19:21:20 +00:00
|
|
|
|
2018-09-20 12:53:13 +00:00
|
|
|
if not cloud.is_logged_in:
|
2019-07-31 19:25:30 +00:00
|
|
|
return {"logged_in": False, "cloud": STATE_DISCONNECTED}
|
2018-09-20 12:53:13 +00:00
|
|
|
|
2017-11-15 07:16:19 +00:00
|
|
|
claims = cloud.claims
|
2019-03-11 19:21:20 +00:00
|
|
|
client = cloud.client
|
2019-03-12 14:54:04 +00:00
|
|
|
remote = cloud.remote
|
|
|
|
|
|
|
|
# Load remote certificate
|
|
|
|
if remote.certificate:
|
|
|
|
certificate = attr.asdict(remote.certificate)
|
|
|
|
else:
|
|
|
|
certificate = None
|
2017-11-15 07:16:19 +00:00
|
|
|
|
2017-09-12 16:47:04 +00:00
|
|
|
return {
|
2019-07-31 19:25:30 +00:00
|
|
|
"logged_in": True,
|
|
|
|
"email": claims["email"],
|
|
|
|
"cloud": cloud.iot.state,
|
|
|
|
"prefs": client.prefs.as_dict(),
|
|
|
|
"google_entities": client.google_user_config["filter"].config,
|
|
|
|
"alexa_entities": client.alexa_user_config["filter"].config,
|
|
|
|
"remote_domain": remote.instance_domain,
|
|
|
|
"remote_connected": remote.is_connected,
|
|
|
|
"remote_certificate": certificate,
|
2017-09-12 16:47:04 +00:00
|
|
|
}
|
2019-03-12 14:54:04 +00:00
|
|
|
|
|
|
|
|
2019-04-01 08:22:51 +00:00
|
|
|
@websocket_api.require_admin
|
2019-03-12 14:54:04 +00:00
|
|
|
@_require_cloud_login
|
|
|
|
@websocket_api.async_response
|
2019-03-16 02:26:10 +00:00
|
|
|
@_ws_handle_cloud_errors
|
2019-07-31 19:25:30 +00:00
|
|
|
@websocket_api.websocket_command({"type": "cloud/remote/connect"})
|
2019-03-12 14:54:04 +00:00
|
|
|
async def websocket_remote_connect(hass, connection, msg):
|
|
|
|
"""Handle request for connect remote."""
|
|
|
|
cloud = hass.data[DOMAIN]
|
|
|
|
await cloud.client.prefs.async_update(remote_enabled=True)
|
2019-03-16 02:26:10 +00:00
|
|
|
await cloud.remote.connect()
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_result(msg["id"], _account_data(cloud))
|
2019-03-12 14:54:04 +00:00
|
|
|
|
|
|
|
|
2019-04-01 08:22:51 +00:00
|
|
|
@websocket_api.require_admin
|
2019-03-12 14:54:04 +00:00
|
|
|
@_require_cloud_login
|
|
|
|
@websocket_api.async_response
|
2019-03-16 02:26:10 +00:00
|
|
|
@_ws_handle_cloud_errors
|
2019-07-31 19:25:30 +00:00
|
|
|
@websocket_api.websocket_command({"type": "cloud/remote/disconnect"})
|
2019-03-12 14:54:04 +00:00
|
|
|
async def websocket_remote_disconnect(hass, connection, msg):
|
|
|
|
"""Handle request for disconnect remote."""
|
|
|
|
cloud = hass.data[DOMAIN]
|
|
|
|
await cloud.client.prefs.async_update(remote_enabled=False)
|
2019-03-16 02:26:10 +00:00
|
|
|
await cloud.remote.disconnect()
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_result(msg["id"], _account_data(cloud))
|
2019-05-29 15:39:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.require_admin
|
|
|
|
@_require_cloud_login
|
|
|
|
@websocket_api.async_response
|
|
|
|
@_ws_handle_cloud_errors
|
2019-07-31 19:25:30 +00:00
|
|
|
@websocket_api.websocket_command({"type": "cloud/google_assistant/entities"})
|
2019-05-29 15:39:12 +00:00
|
|
|
async def google_assistant_list(hass, connection, msg):
|
|
|
|
"""List all google assistant entities."""
|
|
|
|
cloud = hass.data[DOMAIN]
|
2019-11-28 13:23:59 +00:00
|
|
|
gconf = await cloud.client.get_google_config()
|
|
|
|
entities = google_helpers.async_get_entities(hass, gconf)
|
2019-05-29 15:39:12 +00:00
|
|
|
|
|
|
|
result = []
|
|
|
|
|
|
|
|
for entity in entities:
|
2019-07-31 19:25:30 +00:00
|
|
|
result.append(
|
|
|
|
{
|
|
|
|
"entity_id": entity.entity_id,
|
|
|
|
"traits": [trait.name for trait in entity.traits()],
|
2020-04-15 15:43:43 +00:00
|
|
|
"might_2fa": entity.might_2fa_traits(),
|
2019-07-31 19:25:30 +00:00
|
|
|
}
|
|
|
|
)
|
2019-05-29 15:39:12 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_result(msg["id"], result)
|
2019-05-29 15:39:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.require_admin
|
|
|
|
@_require_cloud_login
|
|
|
|
@websocket_api.async_response
|
|
|
|
@_ws_handle_cloud_errors
|
2019-07-31 19:25:30 +00:00
|
|
|
@websocket_api.websocket_command(
|
|
|
|
{
|
|
|
|
"type": "cloud/google_assistant/entities/update",
|
|
|
|
"entity_id": str,
|
|
|
|
vol.Optional("should_expose"): bool,
|
|
|
|
vol.Optional("override_name"): str,
|
|
|
|
vol.Optional("aliases"): [str],
|
|
|
|
vol.Optional("disable_2fa"): bool,
|
|
|
|
}
|
|
|
|
)
|
2019-05-29 15:39:12 +00:00
|
|
|
async def google_assistant_update(hass, connection, msg):
|
2019-06-13 15:43:57 +00:00
|
|
|
"""Update google assistant config."""
|
2019-05-29 15:39:12 +00:00
|
|
|
cloud = hass.data[DOMAIN]
|
|
|
|
changes = dict(msg)
|
2019-07-31 19:25:30 +00:00
|
|
|
changes.pop("type")
|
|
|
|
changes.pop("id")
|
2019-05-29 15:39:12 +00:00
|
|
|
|
|
|
|
await cloud.client.prefs.async_update_google_entity_config(**changes)
|
|
|
|
|
|
|
|
connection.send_result(
|
2019-07-31 19:25:30 +00:00
|
|
|
msg["id"], cloud.client.prefs.google_entity_configs.get(msg["entity_id"])
|
|
|
|
)
|
2019-06-13 18:58:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.require_admin
|
|
|
|
@_require_cloud_login
|
|
|
|
@websocket_api.async_response
|
|
|
|
@_ws_handle_cloud_errors
|
2019-07-31 19:25:30 +00:00
|
|
|
@websocket_api.websocket_command({"type": "cloud/alexa/entities"})
|
2019-06-13 18:58:08 +00:00
|
|
|
async def alexa_list(hass, connection, msg):
|
|
|
|
"""List all alexa entities."""
|
|
|
|
cloud = hass.data[DOMAIN]
|
2019-07-31 19:25:30 +00:00
|
|
|
entities = alexa_entities.async_get_entities(hass, cloud.client.alexa_config)
|
2019-06-13 18:58:08 +00:00
|
|
|
|
|
|
|
result = []
|
|
|
|
|
|
|
|
for entity in entities:
|
2019-07-31 19:25:30 +00:00
|
|
|
result.append(
|
|
|
|
{
|
|
|
|
"entity_id": entity.entity_id,
|
|
|
|
"display_categories": entity.default_display_categories(),
|
|
|
|
"interfaces": [ifc.name() for ifc in entity.interfaces()],
|
|
|
|
}
|
|
|
|
)
|
2019-06-13 18:58:08 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_result(msg["id"], result)
|
2019-06-13 18:58:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.require_admin
|
|
|
|
@_require_cloud_login
|
|
|
|
@websocket_api.async_response
|
|
|
|
@_ws_handle_cloud_errors
|
2019-07-31 19:25:30 +00:00
|
|
|
@websocket_api.websocket_command(
|
|
|
|
{
|
|
|
|
"type": "cloud/alexa/entities/update",
|
|
|
|
"entity_id": str,
|
|
|
|
vol.Optional("should_expose"): bool,
|
|
|
|
}
|
|
|
|
)
|
2019-06-13 18:58:08 +00:00
|
|
|
async def alexa_update(hass, connection, msg):
|
|
|
|
"""Update alexa entity config."""
|
|
|
|
cloud = hass.data[DOMAIN]
|
|
|
|
changes = dict(msg)
|
2019-07-31 19:25:30 +00:00
|
|
|
changes.pop("type")
|
|
|
|
changes.pop("id")
|
2019-06-13 18:58:08 +00:00
|
|
|
|
|
|
|
await cloud.client.prefs.async_update_alexa_entity_config(**changes)
|
|
|
|
|
|
|
|
connection.send_result(
|
2019-07-31 19:25:30 +00:00
|
|
|
msg["id"], cloud.client.prefs.alexa_entity_configs.get(msg["entity_id"])
|
|
|
|
)
|
2019-06-19 08:06:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.require_admin
|
|
|
|
@_require_cloud_login
|
|
|
|
@websocket_api.async_response
|
2019-07-31 19:25:30 +00:00
|
|
|
@websocket_api.websocket_command({"type": "cloud/alexa/sync"})
|
2019-06-19 08:06:29 +00:00
|
|
|
async def alexa_sync(hass, connection, msg):
|
|
|
|
"""Sync with Alexa."""
|
|
|
|
cloud = hass.data[DOMAIN]
|
|
|
|
|
|
|
|
with async_timeout.timeout(10):
|
2019-06-25 05:04:31 +00:00
|
|
|
try:
|
|
|
|
success = await cloud.client.alexa_config.async_sync_entities()
|
|
|
|
except alexa_errors.NoTokenAvailable:
|
|
|
|
connection.send_error(
|
2019-07-31 19:25:30 +00:00
|
|
|
msg["id"],
|
|
|
|
"alexa_relink",
|
2020-01-02 19:17:10 +00:00
|
|
|
"Please go to the Alexa app and re-link the Home Assistant skill.",
|
2019-06-25 05:04:31 +00:00
|
|
|
)
|
|
|
|
return
|
2019-06-19 08:06:29 +00:00
|
|
|
|
|
|
|
if success:
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_result(msg["id"])
|
2019-06-19 08:06:29 +00:00
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, "Unknown error")
|
2019-11-08 09:06:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
@websocket_api.async_response
|
|
|
|
@websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str})
|
|
|
|
async def thingtalk_convert(hass, connection, msg):
|
|
|
|
"""Convert a query."""
|
|
|
|
cloud = hass.data[DOMAIN]
|
|
|
|
|
|
|
|
with async_timeout.timeout(10):
|
|
|
|
try:
|
|
|
|
connection.send_result(
|
|
|
|
msg["id"], await thingtalk.async_convert(cloud, msg["query"])
|
|
|
|
)
|
|
|
|
except thingtalk.ThingTalkConversionError as err:
|
|
|
|
connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, str(err))
|