diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 55a6f1ac61582..4d493ad6b9273 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__) @@ -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 c1165091e1153..063a9daf00a43 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 self._prefs.remote_enabled + @property def alexa_config(self) -> alexa_sh.Config: """Return Alexa config.""" diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 642672f537c4c..65e026389f05a 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 dd8d740f23404..0366675438a8f 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 @@ -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) @@ -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, @@ -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)) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 32362df2fa98f..263c17935cbaf 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.""" diff --git a/requirements_all.txt b/requirements_all.txt index 08b4f1bb5df0b..f1ef6c7f41902 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 aa2f7e8fc6f8e..42351115b6108 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 diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 50b31dd780f02..3ab4b1030fa08 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 @@ -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.""" @@ -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) @@ -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, } @@ -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,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 diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 818e67c98044f..d3e2e50f3a7d1 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):