Google assistant sync (#13392)
* Add Google Assistant Sync API * Update const.py * Async/awaitpull/13419/head
parent
2532d67b9a
commit
4bd6776443
|
@ -37,6 +37,7 @@ CONF_FILTER = 'filter'
|
||||||
CONF_GOOGLE_ACTIONS = 'google_actions'
|
CONF_GOOGLE_ACTIONS = 'google_actions'
|
||||||
CONF_RELAYER = 'relayer'
|
CONF_RELAYER = 'relayer'
|
||||||
CONF_USER_POOL_ID = 'user_pool_id'
|
CONF_USER_POOL_ID = 'user_pool_id'
|
||||||
|
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
|
||||||
|
|
||||||
DEFAULT_MODE = 'production'
|
DEFAULT_MODE = 'production'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
|
@ -75,6 +76,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_USER_POOL_ID): str,
|
vol.Optional(CONF_USER_POOL_ID): str,
|
||||||
vol.Optional(CONF_REGION): str,
|
vol.Optional(CONF_REGION): str,
|
||||||
vol.Optional(CONF_RELAYER): str,
|
vol.Optional(CONF_RELAYER): str,
|
||||||
|
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
|
||||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
||||||
}),
|
}),
|
||||||
|
@ -110,7 +112,7 @@ class Cloud:
|
||||||
|
|
||||||
def __init__(self, hass, mode, alexa, google_actions,
|
def __init__(self, hass, mode, alexa, google_actions,
|
||||||
cognito_client_id=None, user_pool_id=None, region=None,
|
cognito_client_id=None, user_pool_id=None, region=None,
|
||||||
relayer=None):
|
relayer=None, google_actions_sync_url=None):
|
||||||
"""Create an instance of Cloud."""
|
"""Create an instance of Cloud."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
|
@ -128,6 +130,7 @@ class Cloud:
|
||||||
self.user_pool_id = user_pool_id
|
self.user_pool_id = user_pool_id
|
||||||
self.region = region
|
self.region = region
|
||||||
self.relayer = relayer
|
self.relayer = relayer
|
||||||
|
self.google_actions_sync_url = google_actions_sync_url
|
||||||
|
|
||||||
else:
|
else:
|
||||||
info = SERVERS[mode]
|
info = SERVERS[mode]
|
||||||
|
@ -136,6 +139,7 @@ class Cloud:
|
||||||
self.user_pool_id = info['user_pool_id']
|
self.user_pool_id = info['user_pool_id']
|
||||||
self.region = info['region']
|
self.region = info['region']
|
||||||
self.relayer = info['relayer']
|
self.relayer = info['relayer']
|
||||||
|
self.google_actions_sync_url = info['google_actions_sync_url']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_logged_in(self):
|
def is_logged_in(self):
|
||||||
|
|
|
@ -8,7 +8,9 @@ SERVERS = {
|
||||||
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
|
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
|
||||||
'user_pool_id': 'us-east-1_87ll5WOP8',
|
'user_pool_id': 'us-east-1_87ll5WOP8',
|
||||||
'region': 'us-east-1',
|
'region': 'us-east-1',
|
||||||
'relayer': 'wss://cloud.hass.io:8000/websocket'
|
'relayer': 'wss://cloud.hass.io:8000/websocket',
|
||||||
|
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
|
||||||
|
'amazonaws.com/prod/smart_home_sync'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,9 +16,9 @@ from .const import DOMAIN, REQUEST_TIMEOUT
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup(hass):
|
||||||
def async_setup(hass):
|
|
||||||
"""Initialize the HTTP API."""
|
"""Initialize the HTTP API."""
|
||||||
|
hass.http.register_view(GoogleActionsSyncView)
|
||||||
hass.http.register_view(CloudLoginView)
|
hass.http.register_view(CloudLoginView)
|
||||||
hass.http.register_view(CloudLogoutView)
|
hass.http.register_view(CloudLogoutView)
|
||||||
hass.http.register_view(CloudAccountView)
|
hass.http.register_view(CloudAccountView)
|
||||||
|
@ -38,12 +38,11 @@ _CLOUD_ERRORS = {
|
||||||
|
|
||||||
def _handle_cloud_errors(handler):
|
def _handle_cloud_errors(handler):
|
||||||
"""Handle auth errors."""
|
"""Handle auth errors."""
|
||||||
@asyncio.coroutine
|
|
||||||
@wraps(handler)
|
@wraps(handler)
|
||||||
def error_handler(view, request, *args, **kwargs):
|
async def error_handler(view, request, *args, **kwargs):
|
||||||
"""Handle exceptions that raise from the wrapped request handler."""
|
"""Handle exceptions that raise from the wrapped request handler."""
|
||||||
try:
|
try:
|
||||||
result = yield from handler(view, request, *args, **kwargs)
|
result = await handler(view, request, *args, **kwargs)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except (auth_api.CloudError, asyncio.TimeoutError) as err:
|
except (auth_api.CloudError, asyncio.TimeoutError) as err:
|
||||||
|
@ -57,6 +56,31 @@ def _handle_cloud_errors(handler):
|
||||||
return error_handler
|
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):
|
class CloudLoginView(HomeAssistantView):
|
||||||
"""Login to Home Assistant cloud."""
|
"""Login to Home Assistant cloud."""
|
||||||
|
|
||||||
|
@ -68,19 +92,18 @@ class CloudLoginView(HomeAssistantView):
|
||||||
vol.Required('email'): str,
|
vol.Required('email'): str,
|
||||||
vol.Required('password'): str,
|
vol.Required('password'): str,
|
||||||
}))
|
}))
|
||||||
@asyncio.coroutine
|
async def post(self, request, data):
|
||||||
def post(self, request, data):
|
|
||||||
"""Handle login request."""
|
"""Handle login request."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
|
|
||||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||||
yield from hass.async_add_job(auth_api.login, cloud, data['email'],
|
await hass.async_add_job(auth_api.login, cloud, data['email'],
|
||||||
data['password'])
|
data['password'])
|
||||||
|
|
||||||
hass.async_add_job(cloud.iot.connect)
|
hass.async_add_job(cloud.iot.connect)
|
||||||
# Allow cloud to start connecting.
|
# Allow cloud to start connecting.
|
||||||
yield from asyncio.sleep(0, loop=hass.loop)
|
await asyncio.sleep(0, loop=hass.loop)
|
||||||
return self.json(_account_data(cloud))
|
return self.json(_account_data(cloud))
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,14 +114,13 @@ class CloudLogoutView(HomeAssistantView):
|
||||||
name = 'api:cloud:logout'
|
name = 'api:cloud:logout'
|
||||||
|
|
||||||
@_handle_cloud_errors
|
@_handle_cloud_errors
|
||||||
@asyncio.coroutine
|
async def post(self, request):
|
||||||
def post(self, request):
|
|
||||||
"""Handle logout request."""
|
"""Handle logout request."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
|
|
||||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||||
yield from cloud.logout()
|
await cloud.logout()
|
||||||
|
|
||||||
return self.json_message('ok')
|
return self.json_message('ok')
|
||||||
|
|
||||||
|
@ -109,8 +131,7 @@ class CloudAccountView(HomeAssistantView):
|
||||||
url = '/api/cloud/account'
|
url = '/api/cloud/account'
|
||||||
name = 'api:cloud:account'
|
name = 'api:cloud:account'
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def get(self, request):
|
||||||
def get(self, request):
|
|
||||||
"""Get account info."""
|
"""Get account info."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
|
@ -132,14 +153,13 @@ class CloudRegisterView(HomeAssistantView):
|
||||||
vol.Required('email'): str,
|
vol.Required('email'): str,
|
||||||
vol.Required('password'): vol.All(str, vol.Length(min=6)),
|
vol.Required('password'): vol.All(str, vol.Length(min=6)),
|
||||||
}))
|
}))
|
||||||
@asyncio.coroutine
|
async def post(self, request, data):
|
||||||
def post(self, request, data):
|
|
||||||
"""Handle registration request."""
|
"""Handle registration request."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
|
|
||||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||||
yield from hass.async_add_job(
|
await hass.async_add_job(
|
||||||
auth_api.register, cloud, data['email'], data['password'])
|
auth_api.register, cloud, data['email'], data['password'])
|
||||||
|
|
||||||
return self.json_message('ok')
|
return self.json_message('ok')
|
||||||
|
@ -155,14 +175,13 @@ class CloudResendConfirmView(HomeAssistantView):
|
||||||
@RequestDataValidator(vol.Schema({
|
@RequestDataValidator(vol.Schema({
|
||||||
vol.Required('email'): str,
|
vol.Required('email'): str,
|
||||||
}))
|
}))
|
||||||
@asyncio.coroutine
|
async def post(self, request, data):
|
||||||
def post(self, request, data):
|
|
||||||
"""Handle resending confirm email code request."""
|
"""Handle resending confirm email code request."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
|
|
||||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||||
yield from hass.async_add_job(
|
await hass.async_add_job(
|
||||||
auth_api.resend_email_confirm, cloud, data['email'])
|
auth_api.resend_email_confirm, cloud, data['email'])
|
||||||
|
|
||||||
return self.json_message('ok')
|
return self.json_message('ok')
|
||||||
|
@ -178,14 +197,13 @@ class CloudForgotPasswordView(HomeAssistantView):
|
||||||
@RequestDataValidator(vol.Schema({
|
@RequestDataValidator(vol.Schema({
|
||||||
vol.Required('email'): str,
|
vol.Required('email'): str,
|
||||||
}))
|
}))
|
||||||
@asyncio.coroutine
|
async def post(self, request, data):
|
||||||
def post(self, request, data):
|
|
||||||
"""Handle forgot password request."""
|
"""Handle forgot password request."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
|
|
||||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||||
yield from hass.async_add_job(
|
await hass.async_add_job(
|
||||||
auth_api.forgot_password, cloud, data['email'])
|
auth_api.forgot_password, cloud, data['email'])
|
||||||
|
|
||||||
return self.json_message('ok')
|
return self.json_message('ok')
|
||||||
|
|
|
@ -11,6 +11,9 @@ from homeassistant.components.cloud import DOMAIN, auth_api, iot
|
||||||
from tests.common import mock_coro
|
from tests.common import mock_coro
|
||||||
|
|
||||||
|
|
||||||
|
GOOGLE_ACTIONS_SYNC_URL = 'https://api-test.hass.io/google_actions_sync'
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cloud_client(hass, aiohttp_client):
|
def cloud_client(hass, aiohttp_client):
|
||||||
"""Fixture that can fetch from the cloud client."""
|
"""Fixture that can fetch from the cloud client."""
|
||||||
|
@ -23,6 +26,7 @@ def cloud_client(hass, aiohttp_client):
|
||||||
'user_pool_id': 'user_pool_id',
|
'user_pool_id': 'user_pool_id',
|
||||||
'region': 'region',
|
'region': 'region',
|
||||||
'relayer': 'relayer',
|
'relayer': 'relayer',
|
||||||
|
'google_actions_sync_url': GOOGLE_ACTIONS_SYNC_URL,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
hass.data['cloud']._decode_claims = \
|
hass.data['cloud']._decode_claims = \
|
||||||
|
@ -38,6 +42,21 @@ def mock_cognito():
|
||||||
yield mock_cog()
|
yield mock_cog()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_google_actions_sync(mock_cognito, cloud_client, aioclient_mock):
|
||||||
|
"""Test syncing Google Actions."""
|
||||||
|
aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL)
|
||||||
|
req = await cloud_client.post('/api/cloud/google_actions/sync')
|
||||||
|
assert req.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
async def test_google_actions_sync_fails(mock_cognito, cloud_client,
|
||||||
|
aioclient_mock):
|
||||||
|
"""Test syncing Google Actions gone bad."""
|
||||||
|
aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL, status=403)
|
||||||
|
req = await cloud_client.post('/api/cloud/google_actions/sync')
|
||||||
|
assert req.status == 403
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_account_view_no_account(cloud_client):
|
def test_account_view_no_account(cloud_client):
|
||||||
"""Test fetching account if no account available."""
|
"""Test fetching account if no account available."""
|
||||||
|
|
|
@ -29,6 +29,7 @@ def test_constructor_loads_info_from_constant():
|
||||||
'user_pool_id': 'test-user_pool_id',
|
'user_pool_id': 'test-user_pool_id',
|
||||||
'region': 'test-region',
|
'region': 'test-region',
|
||||||
'relayer': 'test-relayer',
|
'relayer': 'test-relayer',
|
||||||
|
'google_actions_sync_url': 'test-google_actions_sync_url',
|
||||||
}
|
}
|
||||||
}), patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset',
|
}), patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset',
|
||||||
return_value=mock_coro(True)):
|
return_value=mock_coro(True)):
|
||||||
|
@ -43,6 +44,7 @@ def test_constructor_loads_info_from_constant():
|
||||||
assert cl.user_pool_id == 'test-user_pool_id'
|
assert cl.user_pool_id == 'test-user_pool_id'
|
||||||
assert cl.region == 'test-region'
|
assert cl.region == 'test-region'
|
||||||
assert cl.relayer == 'test-relayer'
|
assert cl.relayer == 'test-relayer'
|
||||||
|
assert cl.google_actions_sync_url == 'test-google_actions_sync_url'
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
Loading…
Reference in New Issue