Skip to content

Commit

Permalink
Add cloud status (#21960)
Browse files Browse the repository at this point in the history
* Add cloud status

* Expose certificate details

* store & reset last state

* Fix tests

* update tests

* update req

* fix lint
  • Loading branch information
balloob authored and pvizeli committed Mar 12, 2019
1 parent ac97ceb commit d3bab30
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 47 deletions.
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

0 comments on commit d3bab30

Please sign in to comment.