Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cloud status #21960

Merged
merged 7 commits into from
Mar 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion homeassistant/components/cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
CONF_USER_POOL_ID, DOMAIN, MODE_DEV, MODE_PROD)
from .prefs import CloudPreferences

REQUIREMENTS = ['hass-nabucasa==0.3']
REQUIREMENTS = ['hass-nabucasa==0.4']
DEPENDENCIES = ['http']

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -162,8 +162,10 @@ async def _service_handler(service):
"""Handle service for cloud."""
if service.service == SERVICE_REMOTE_CONNECT:
await cloud.remote.connect()
await prefs.async_update(remote_enabled=True)
elif service.service == SERVICE_REMOTE_DISCONNECT:
await cloud.remote.disconnect()
await prefs.async_update(remote_enabled=False)

hass.services.async_register(
DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler)
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/cloud/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ def cloudhooks(self) -> Dict[str, Dict[str, str]]:
"""Return list of cloudhooks."""
return self._prefs.cloudhooks

@property
def remote_autostart(self) -> bool:
"""Return true if we want start a remote connection."""
return self._prefs.remote_enabled

@property
def alexa_config(self) -> alexa_sh.Config:
"""Return Alexa config."""
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/cloud/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

PREF_ENABLE_ALEXA = 'alexa_enabled'
PREF_ENABLE_GOOGLE = 'google_enabled'
PREF_ENABLE_REMOTE = 'remote_enabled'
PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock'
PREF_CLOUDHOOKS = 'cloudhooks'

Expand Down
41 changes: 41 additions & 0 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 attr
import aiohttp
import async_timeout
import voluptuous as vol
Expand Down Expand Up @@ -82,6 +83,10 @@ async def async_setup(hass):
WS_TYPE_HOOK_DELETE, websocket_hook_delete,
SCHEMA_WS_HOOK_DELETE
)
hass.components.websocket_api.async_register_command(
websocket_remote_connect)
hass.components.websocket_api.async_register_command(
websocket_remote_disconnect)
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
Expand Down Expand Up @@ -387,6 +392,13 @@ def _account_data(cloud):

claims = cloud.claims
client = cloud.client
remote = cloud.remote

# Load remote certificate
if remote.certificate:
certificate = attr.asdict(remote.certificate)
else:
certificate = None

return {
'logged_in': True,
Expand All @@ -397,4 +409,33 @@ def _account_data(cloud):
'google_domains': list(google_sh.DOMAIN_TO_GOOGLE_TYPES),
'alexa_entities': client.alexa_config.should_expose.config,
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
'remote_domain': remote.instance_domain,
'remote_connected': remote.is_connected,
'remote_certificate': certificate,
}


@_require_cloud_login
@websocket_api.async_response
@websocket_api.websocket_command({
'type': 'cloud/remote/connect'
})
async def websocket_remote_connect(hass, connection, msg):
"""Handle request for connect remote."""
cloud = hass.data[DOMAIN]
await cloud.remote.connect()
await cloud.client.prefs.async_update(remote_enabled=True)
connection.send_result(msg['id'], _account_data(cloud))


@_require_cloud_login
@websocket_api.async_response
@websocket_api.websocket_command({
'type': 'cloud/remote/disconnect'
})
async def websocket_remote_disconnect(hass, connection, msg):
"""Handle request for disconnect remote."""
cloud = hass.data[DOMAIN]
await cloud.remote.disconnect()
await cloud.client.prefs.async_update(remote_enabled=False)
connection.send_result(msg['id'], _account_data(cloud))
13 changes: 10 additions & 3 deletions homeassistant/components/cloud/prefs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Preference management for cloud."""
from .const import (
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS)

STORAGE_KEY = DOMAIN
Expand All @@ -24,19 +24,21 @@ async def async_initialize(self):
prefs = {
PREF_ENABLE_ALEXA: True,
PREF_ENABLE_GOOGLE: True,
PREF_ENABLE_REMOTE: False,
PREF_GOOGLE_ALLOW_UNLOCK: False,
PREF_CLOUDHOOKS: {}
}

self._prefs = prefs

async def async_update(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF, google_allow_unlock=_UNDEF,
cloudhooks=_UNDEF):
alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
google_allow_unlock=_UNDEF, cloudhooks=_UNDEF):
"""Update user preferences."""
for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled),
(PREF_ENABLE_ALEXA, alexa_enabled),
(PREF_ENABLE_REMOTE, remote_enabled),
(PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock),
(PREF_CLOUDHOOKS, cloudhooks),
):
Expand All @@ -49,6 +51,11 @@ def as_dict(self):
"""Return dictionary version."""
return self._prefs

@property
def remote_enabled(self):
"""Return if remote is enabled on start."""
return self._prefs.get(PREF_ENABLE_REMOTE, False)

@property
def alexa_enabled(self):
"""Return if Alexa is enabled."""
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ habitipy==0.2.0
hangups==0.4.6

# homeassistant.components.cloud
hass-nabucasa==0.3
hass-nabucasa==0.4

# homeassistant.components.mqtt.server
hbmqtt==0.9.4
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ ha-ffmpeg==1.11
hangups==0.4.6

# homeassistant.components.cloud
hass-nabucasa==0.3
hass-nabucasa==0.4

# homeassistant.components.mqtt.server
hbmqtt==0.9.4
Expand Down
108 changes: 67 additions & 41 deletions tests/components/cloud/test_http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

from homeassistant.components.cloud.const import (
PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_ALLOW_UNLOCK, DOMAIN)
from homeassistant.util import dt as dt_util

from tests.common import mock_coro

Expand All @@ -26,6 +25,15 @@ def mock_auth():
yield


@pytest.fixture()
def mock_cloud_login(hass, setup_api):
"""Mock cloud is logged in."""
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': '2018-01-03'
}, 'test')


@pytest.fixture(autouse=True)
def setup_api(hass, aioclient_mock):
"""Initialize HTTP API."""
Expand Down Expand Up @@ -319,12 +327,9 @@ async def test_resend_confirm_view_unknown_error(mock_cognito, cloud_client):
assert req.status == 502


async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture):
async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture,
mock_cloud_login):
"""Test querying the status."""
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': '2018-01-03'
}, 'test')
hass.data[DOMAIN].iot.state = STATE_CONNECTED
client = await hass_ws_client(hass)

Expand Down Expand Up @@ -357,6 +362,9 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture):
'exclude_entities': [],
},
'google_domains': ['light'],
'remote_domain': None,
'remote_connected': False,
'remote_certificate': None,
}


Expand All @@ -375,13 +383,9 @@ async def test_websocket_status_not_logged_in(hass, hass_ws_client):


async def test_websocket_subscription_reconnect(
hass, hass_ws_client, aioclient_mock, mock_auth):
hass, hass_ws_client, aioclient_mock, mock_auth, mock_cloud_login):
"""Test querying the status and connecting because valid account."""
aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'provider': 'stripe'})
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': dt_util.utcnow().date().isoformat()
}, 'test')
client = await hass_ws_client(hass)

with patch(
Expand All @@ -403,14 +407,10 @@ async def test_websocket_subscription_reconnect(


async def test_websocket_subscription_no_reconnect_if_connected(
hass, hass_ws_client, aioclient_mock, mock_auth):
hass, hass_ws_client, aioclient_mock, mock_auth, mock_cloud_login):
"""Test querying the status and not reconnecting because still expired."""
aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'provider': 'stripe'})
hass.data[DOMAIN].iot.state = STATE_CONNECTED
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': dt_util.utcnow().date().isoformat()
}, 'test')
client = await hass_ws_client(hass)

with patch(
Expand All @@ -432,13 +432,9 @@ async def test_websocket_subscription_no_reconnect_if_connected(


async def test_websocket_subscription_no_reconnect_if_expired(
hass, hass_ws_client, aioclient_mock, mock_auth):
hass, hass_ws_client, aioclient_mock, mock_auth, mock_cloud_login):
"""Test querying the status and not reconnecting because still expired."""
aioclient_mock.get(SUBSCRIPTION_INFO_URL, json={'provider': 'stripe'})
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': '2018-01-03'
}, 'test')
client = await hass_ws_client(hass)

with patch(
Expand All @@ -460,13 +456,10 @@ async def test_websocket_subscription_no_reconnect_if_expired(


async def test_websocket_subscription_fail(hass, hass_ws_client,
aioclient_mock, mock_auth):
aioclient_mock, mock_auth,
mock_cloud_login):
"""Test querying the status."""
aioclient_mock.get(SUBSCRIPTION_INFO_URL, status=500)
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': '2018-01-03'
}, 'test')
client = await hass_ws_client(hass)
await client.send_json({
'id': 5,
Expand Down Expand Up @@ -494,15 +487,12 @@ async def test_websocket_subscription_not_logged_in(hass, hass_ws_client):


async def test_websocket_update_preferences(hass, hass_ws_client,
aioclient_mock, setup_api):
aioclient_mock, setup_api,
mock_cloud_login):
"""Test updating preference."""
assert setup_api[PREF_ENABLE_GOOGLE]
assert setup_api[PREF_ENABLE_ALEXA]
assert setup_api[PREF_GOOGLE_ALLOW_UNLOCK]
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': '2018-01-03'
}, 'test')
client = await hass_ws_client(hass)
await client.send_json({
'id': 5,
Expand All @@ -519,12 +509,9 @@ async def test_websocket_update_preferences(hass, hass_ws_client,
assert not setup_api[PREF_GOOGLE_ALLOW_UNLOCK]


async def test_enabling_webhook(hass, hass_ws_client, setup_api):
async def test_enabling_webhook(hass, hass_ws_client, setup_api,
mock_cloud_login):
"""Test we call right code to enable webhooks."""
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': '2018-01-03'
}, 'test')
client = await hass_ws_client(hass)
with patch(
'hass_nabucasa.cloudhooks.Cloudhooks.async_create',
Expand All @@ -542,12 +529,9 @@ async def test_enabling_webhook(hass, hass_ws_client, setup_api):
assert mock_enable.mock_calls[0][1][0] == 'mock-webhook-id'


async def test_disabling_webhook(hass, hass_ws_client, setup_api):
async def test_disabling_webhook(hass, hass_ws_client, setup_api,
mock_cloud_login):
"""Test we call right code to disable webhooks."""
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': '2018-01-03'
}, 'test')
client = await hass_ws_client(hass)
with patch(
'hass_nabucasa.cloudhooks.Cloudhooks.async_delete',
Expand All @@ -563,3 +547,45 @@ async def test_disabling_webhook(hass, hass_ws_client, setup_api):

assert len(mock_disable.mock_calls) == 1
assert mock_disable.mock_calls[0][1][0] == 'mock-webhook-id'


async def test_enabling_remote(hass, hass_ws_client, setup_api,
mock_cloud_login):
"""Test we call right code to enable remote UI."""
client = await hass_ws_client(hass)
cloud = hass.data[DOMAIN]

with patch(
'hass_nabucasa.remote.RemoteUI.connect',
return_value=mock_coro()
) as mock_connect:
await client.send_json({
'id': 5,
'type': 'cloud/remote/connect',
})
response = await client.receive_json()
assert response['success']
assert cloud.client.remote_autostart

assert len(mock_connect.mock_calls) == 1


async def test_disabling_remote(hass, hass_ws_client, setup_api,
mock_cloud_login):
"""Test we call right code to disable remote UI."""
client = await hass_ws_client(hass)
cloud = hass.data[DOMAIN]

with patch(
'hass_nabucasa.remote.RemoteUI.disconnect',
return_value=mock_coro()
) as mock_disconnect:
await client.send_json({
'id': 5,
'type': 'cloud/remote/disconnect',
})
response = await client.receive_json()
assert response['success']
assert not cloud.client.remote_autostart

assert len(mock_disconnect.mock_calls) == 1
4 changes: 4 additions & 0 deletions tests/components/cloud/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ async def test_constructor_loads_info_from_config():

async def test_remote_services(hass, mock_cloud_fixture):
"""Setup cloud component and test services."""
cloud = hass.data[DOMAIN]

assert hass.services.has_service(DOMAIN, 'remote_connect')
assert hass.services.has_service(DOMAIN, 'remote_disconnect')

Expand All @@ -48,6 +50,7 @@ async def test_remote_services(hass, mock_cloud_fixture):
await hass.services.async_call(DOMAIN, "remote_connect", blocking=True)

assert mock_connect.called
assert cloud.client.remote_autostart

with patch(
"hass_nabucasa.remote.RemoteUI.disconnect", return_value=mock_coro()
Expand All @@ -56,6 +59,7 @@ async def test_remote_services(hass, mock_cloud_fixture):
DOMAIN, "remote_disconnect", blocking=True)

assert mock_disconnect.called
assert not cloud.client.remote_autostart


async def test_startup_shutdown_events(hass, mock_cloud_fixture):
Expand Down