From fa95c9f84595651e09dfa081562710771964a9cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 11 Mar 2019 23:55:16 -0700 Subject: [PATCH 1/7] Add cloud status --- homeassistant/components/cloud/http_api.py | 33 +++++++ tests/components/cloud/test_http_api.py | 100 ++++++++++++--------- 2 files changed, 93 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index dd8d740f23404b..10a3b5efa981d8 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -82,6 +82,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) @@ -387,6 +391,7 @@ def _account_data(cloud): claims = cloud.claims client = cloud.client + remote = cloud.remote return { 'logged_in': True, @@ -397,4 +402,32 @@ 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), + # pylint: disable=protected-access + 'remote_domain': remote._instance_domain, + 'remote_connected': bool( + remote._snitun and remote._snitun.is_connected) } + + +@_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() + 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() + connection.send_result(msg['id'], _account_data(cloud)) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 50b31dd780f024..1b9b2491f14934 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -26,6 +26,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.""" @@ -319,12 +328,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) @@ -357,6 +363,8 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture): 'exclude_entities': [], }, 'google_domains': ['light'], + 'remote_domain': None, + 'remote_connected': False, } @@ -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( @@ -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( @@ -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( @@ -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, @@ -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, @@ -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', @@ -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', @@ -563,3 +547,39 @@ 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) + 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 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) + 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 len(mock_disconnect.mock_calls) == 1 From 53575399108da8dbfb23878a5419767d74d06cac Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 12 Mar 2019 11:56:39 +0100 Subject: [PATCH 2/7] Expose certificate details --- homeassistant/components/cloud/__init__.py | 2 +- homeassistant/components/cloud/client.py | 5 +++++ homeassistant/components/cloud/http_api.py | 14 ++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 55a6f1ac615820..104fea592e78c9 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -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__) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index c1165091e1153c..31ed83be97db4d 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -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 True + @property def alexa_config(self) -> alexa_sh.Config: """Return Alexa config.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 10a3b5efa981d8..c912e37ec8be4c 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -3,6 +3,7 @@ from functools import wraps import logging +import attr import aiohttp import async_timeout import voluptuous as vol @@ -393,6 +394,12 @@ def _account_data(cloud): client = cloud.client remote = cloud.remote + # Load remote certificate + if remote.certificate: + certificate = attr.asdict(remote.certificate) + else: + certificate = None + return { 'logged_in': True, 'email': claims['email'], @@ -402,10 +409,9 @@ 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), - # pylint: disable=protected-access - 'remote_domain': remote._instance_domain, - 'remote_connected': bool( - remote._snitun and remote._snitun.is_connected) + 'remote_domain': remote.instance_domain, + 'remote_connected': remote.is_connected, + 'remote_certificate': certificate, } From db7b4e0d1b708c3f51d64d82406900b5deb355df Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 12 Mar 2019 12:31:43 +0100 Subject: [PATCH 3/7] store & reset last state --- homeassistant/components/cloud/__init__.py | 2 ++ homeassistant/components/cloud/client.py | 2 +- homeassistant/components/cloud/const.py | 1 + homeassistant/components/cloud/http_api.py | 2 ++ homeassistant/components/cloud/prefs.py | 13 ++++++++++--- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 104fea592e78c9..4d493ad6b9273a 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -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) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 31ed83be97db4d..063a9daf00a435 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -67,7 +67,7 @@ def cloudhooks(self) -> Dict[str, Dict[str, str]]: @property def remote_autostart(self) -> bool: """Return true if we want start a remote connection.""" - return True + return self._prefs.remote_enabled @property def alexa_config(self) -> alexa_sh.Config: diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 642672f537c4cf..65e026389f05a4 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -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' diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index c912e37ec8be4c..0366675438a8f8 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -424,6 +424,7 @@ 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)) @@ -436,4 +437,5 @@ 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)) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 32362df2fa98f8..263c17935cbafe 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -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 @@ -24,6 +24,7 @@ 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: {} } @@ -31,12 +32,13 @@ async def async_initialize(self): 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), ): @@ -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.""" From b39a3a58498f0a57a1ad70f3761735cd4f25e0ae Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 12 Mar 2019 14:29:10 +0100 Subject: [PATCH 4/7] Fix tests --- tests/components/cloud/test_http_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 1b9b2491f14934..fdda8d0d0cb7f6 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -365,6 +365,7 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture, 'google_domains': ['light'], 'remote_domain': None, 'remote_connected': False, + 'remote_certificate': None, } From 149f52a4bef6663822fa6681676b67e77145d6a4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 12 Mar 2019 14:35:02 +0100 Subject: [PATCH 5/7] update tests --- tests/components/cloud/test_http_api.py | 6 ++++++ tests/components/cloud/test_init.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index fdda8d0d0cb7f6..f327950853c467 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -554,6 +554,8 @@ 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() @@ -564,6 +566,7 @@ async def test_enabling_remote(hass, hass_ws_client, setup_api, }) response = await client.receive_json() assert response['success'] + assert cloud.client.remote_autostart assert len(mock_connect.mock_calls) == 1 @@ -572,6 +575,8 @@ 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() @@ -582,5 +587,6 @@ async def test_disabling_remote(hass, hass_ws_client, setup_api, }) response = await client.receive_json() assert response['success'] + assert not cloud.client.remote_autostart assert len(mock_disconnect.mock_calls) == 1 diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 818e67c98044fd..d3e2e50f3a7d14 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -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') @@ -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() @@ -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): From 099609aefe736af3b8099c414e06d6bc59dda0c1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 12 Mar 2019 14:38:04 +0100 Subject: [PATCH 6/7] update req --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 08b4f1bb5df0b0..f1ef6c7f419029 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa2f7e8fc6f8ed..42351115b61081 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 161ed12b288d1920a16157c1c23d3c73ce4e469d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 12 Mar 2019 15:04:03 +0100 Subject: [PATCH 7/7] fix lint --- tests/components/cloud/test_http_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index f327950853c467..3ab4b1030fa081 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -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