From d9c7f777c536936b1dfa2f21a6c6e63ef85db0a0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Nov 2018 23:23:07 +0100 Subject: [PATCH] Add cloud pref for Google unlock (#18600) --- homeassistant/components/cloud/__init__.py | 49 ++--------------- homeassistant/components/cloud/const.py | 4 ++ homeassistant/components/cloud/http_api.py | 14 ++--- homeassistant/components/cloud/iot.py | 4 +- homeassistant/components/cloud/prefs.py | 63 ++++++++++++++++++++++ tests/components/cloud/__init__.py | 8 +-- tests/components/cloud/test_http_api.py | 18 ++++--- tests/components/cloud/test_iot.py | 9 ++-- 8 files changed, 103 insertions(+), 66 deletions(-) create mode 100644 homeassistant/components/cloud/prefs.py diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index d9ee2a62b84..b968850668d 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -20,17 +20,12 @@ from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.google_assistant import helpers as ga_h from homeassistant.components.google_assistant import const as ga_c -from . import http_api, iot, auth_api +from . import http_api, iot, auth_api, prefs from .const import CONFIG_DIR, DOMAIN, SERVERS REQUIREMENTS = ['warrant==0.6.1'] -STORAGE_KEY = DOMAIN -STORAGE_VERSION = 1 -STORAGE_ENABLE_ALEXA = 'alexa_enabled' -STORAGE_ENABLE_GOOGLE = 'google_enabled' _LOGGER = logging.getLogger(__name__) -_UNDEF = object() CONF_ALEXA = 'alexa' CONF_ALIASES = 'aliases' @@ -70,8 +65,6 @@ ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({ GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend({ vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA}, - vol.Optional(ga_c.CONF_ALLOW_UNLOCK, - default=ga_c.DEFAULT_ALLOW_UNLOCK): cv.boolean }) CONFIG_SCHEMA = vol.Schema({ @@ -127,12 +120,11 @@ class Cloud: self.alexa_config = alexa self.google_actions_user_conf = google_actions self._gactions_config = None - self._prefs = None + self.prefs = prefs.CloudPreferences(hass) self.id_token = None self.access_token = None self.refresh_token = None self.iot = iot.CloudIoT(self) - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) if mode == MODE_DEV: self.cognito_client_id = cognito_client_id @@ -196,21 +188,11 @@ class Cloud: should_expose=should_expose, agent_user_id=self.claims['cognito:username'], entity_config=conf.get(CONF_ENTITY_CONFIG), - allow_unlock=conf.get(ga_c.CONF_ALLOW_UNLOCK), + allow_unlock=self.prefs.google_allow_unlock, ) return self._gactions_config - @property - def alexa_enabled(self): - """Return if Alexa is enabled.""" - return self._prefs[STORAGE_ENABLE_ALEXA] - - @property - def google_enabled(self): - """Return if Google is enabled.""" - return self._prefs[STORAGE_ENABLE_GOOGLE] - def path(self, *parts): """Get config path inside cloud dir. @@ -250,20 +232,6 @@ class Cloud: async def async_start(self, _): """Start the cloud component.""" - prefs = await self._store.async_load() - if prefs is None: - prefs = {} - if self.mode not in prefs: - # Default to True if already logged in to make this not a - # breaking change. - enabled = await self.hass.async_add_executor_job( - os.path.isfile, self.user_info_path) - prefs = { - STORAGE_ENABLE_ALEXA: enabled, - STORAGE_ENABLE_GOOGLE: enabled, - } - self._prefs = prefs - def load_config(): """Load config.""" # Ensure config dir exists @@ -280,6 +248,8 @@ class Cloud: info = await self.hass.async_add_job(load_config) + await self.prefs.async_initialize(not info) + if info is None: return @@ -289,15 +259,6 @@ class Cloud: self.hass.add_job(self.iot.connect()) - async def update_preferences(self, *, google_enabled=_UNDEF, - alexa_enabled=_UNDEF): - """Update user preferences.""" - if google_enabled is not _UNDEF: - self._prefs[STORAGE_ENABLE_GOOGLE] = google_enabled - if alexa_enabled is not _UNDEF: - self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled - await self._store.async_save(self._prefs) - def _decode_claims(self, token): # pylint: disable=no-self-use """Decode the claims in a token.""" from jose import jwt diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 88fb88474a1..abc72da796c 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -3,6 +3,10 @@ DOMAIN = 'cloud' CONFIG_DIR = '.cloud' REQUEST_TIMEOUT = 10 +PREF_ENABLE_ALEXA = 'alexa_enabled' +PREF_ENABLE_GOOGLE = 'google_enabled' +PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock' + SERVERS = { 'production': { 'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u', diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index cb62d773dfd..7b509f4eae2 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -15,7 +15,9 @@ from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.google_assistant import smart_home as google_sh from . import auth_api -from .const import DOMAIN, REQUEST_TIMEOUT +from .const import ( + DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, + PREF_GOOGLE_ALLOW_UNLOCK) from .iot import STATE_DISCONNECTED, STATE_CONNECTED _LOGGER = logging.getLogger(__name__) @@ -30,8 +32,9 @@ SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ WS_TYPE_UPDATE_PREFS = 'cloud/update_prefs' SCHEMA_WS_UPDATE_PREFS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_UPDATE_PREFS, - vol.Optional('google_enabled'): bool, - vol.Optional('alexa_enabled'): bool, + vol.Optional(PREF_ENABLE_GOOGLE): bool, + vol.Optional(PREF_ENABLE_ALEXA): bool, + vol.Optional(PREF_GOOGLE_ALLOW_UNLOCK): bool, }) @@ -288,7 +291,7 @@ async def websocket_update_prefs(hass, connection, msg): changes = dict(msg) changes.pop('id') changes.pop('type') - await cloud.update_preferences(**changes) + await cloud.prefs.async_update(**changes) connection.send_message(websocket_api.result_message( msg['id'], {'success': True})) @@ -308,10 +311,9 @@ def _account_data(cloud): 'logged_in': True, 'email': claims['email'], 'cloud': cloud.iot.state, - 'google_enabled': cloud.google_enabled, + 'prefs': cloud.prefs.as_dict(), 'google_entities': cloud.google_actions_user_conf['filter'].config, 'google_domains': list(google_sh.DOMAIN_TO_GOOGLE_TYPES), - 'alexa_enabled': cloud.alexa_enabled, 'alexa_entities': cloud.alexa_config.should_expose.config, 'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS), } diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index b4f228a630d..c5657ae9729 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -229,7 +229,7 @@ def async_handle_alexa(hass, cloud, payload): """Handle an incoming IoT message for Alexa.""" result = yield from alexa.async_handle_message( hass, cloud.alexa_config, payload, - enabled=cloud.alexa_enabled) + enabled=cloud.prefs.alexa_enabled) return result @@ -237,7 +237,7 @@ def async_handle_alexa(hass, cloud, payload): @asyncio.coroutine def async_handle_google_actions(hass, cloud, payload): """Handle an incoming IoT message for Google Actions.""" - if not cloud.google_enabled: + if not cloud.prefs.google_enabled: return ga.turned_off_response(payload) result = yield from ga.async_handle_message( diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py new file mode 100644 index 00000000000..d29b356cfc0 --- /dev/null +++ b/homeassistant/components/cloud/prefs.py @@ -0,0 +1,63 @@ +"""Preference management for cloud.""" +from .const import ( + DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, + PREF_GOOGLE_ALLOW_UNLOCK) + +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 +_UNDEF = object() + + +class CloudPreferences: + """Handle cloud preferences.""" + + def __init__(self, hass): + """Initialize cloud prefs.""" + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._prefs = None + + async def async_initialize(self, logged_in): + """Finish initializing the preferences.""" + prefs = await self._store.async_load() + + if prefs is None: + # Backwards compat: we enable alexa/google if already logged in + prefs = { + PREF_ENABLE_ALEXA: logged_in, + PREF_ENABLE_GOOGLE: logged_in, + PREF_GOOGLE_ALLOW_UNLOCK: False, + } + + self._prefs = prefs + + async def async_update(self, *, google_enabled=_UNDEF, + alexa_enabled=_UNDEF, google_allow_unlock=_UNDEF): + """Update user preferences.""" + for key, value in ( + (PREF_ENABLE_GOOGLE, google_enabled), + (PREF_ENABLE_ALEXA, alexa_enabled), + (PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock), + ): + if value is not _UNDEF: + self._prefs[key] = value + + await self._store.async_save(self._prefs) + + def as_dict(self): + """Return dictionary version.""" + return self._prefs + + @property + def alexa_enabled(self): + """Return if Alexa is enabled.""" + return self._prefs[PREF_ENABLE_ALEXA] + + @property + def google_enabled(self): + """Return if Google is enabled.""" + return self._prefs[PREF_ENABLE_GOOGLE] + + @property + def google_allow_unlock(self): + """Return if Google is allowed to unlock locks.""" + return self._prefs.get(PREF_GOOGLE_ALLOW_UNLOCK, False) diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 108e5c45137..ba63e43d091 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -2,6 +2,7 @@ from unittest.mock import patch from homeassistant.setup import async_setup_component from homeassistant.components import cloud +from homeassistant.components.cloud import const from jose import jwt @@ -24,9 +25,10 @@ def mock_cloud(hass, config={}): def mock_cloud_prefs(hass, prefs={}): """Fixture for cloud component.""" prefs_to_set = { - cloud.STORAGE_ENABLE_ALEXA: True, - cloud.STORAGE_ENABLE_GOOGLE: True, + const.PREF_ENABLE_ALEXA: True, + const.PREF_ENABLE_GOOGLE: True, + const.PREF_GOOGLE_ALLOW_UNLOCK: True, } prefs_to_set.update(prefs) - hass.data[cloud.DOMAIN]._prefs = prefs_to_set + hass.data[cloud.DOMAIN].prefs._prefs = prefs_to_set return prefs_to_set diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index a8128c8d3e0..4abf5b8501d 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -6,7 +6,9 @@ import pytest from jose import jwt from homeassistant.components.cloud import ( - DOMAIN, auth_api, iot, STORAGE_ENABLE_GOOGLE, STORAGE_ENABLE_ALEXA) + DOMAIN, auth_api, iot) +from homeassistant.components.cloud.const import ( + PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_ALLOW_UNLOCK) from homeassistant.util import dt as dt_util from tests.common import mock_coro @@ -350,7 +352,7 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture): 'logged_in': True, 'email': 'hello@home-assistant.io', 'cloud': 'connected', - 'alexa_enabled': True, + 'prefs': mock_cloud_fixture, 'alexa_entities': { 'include_domains': [], 'include_entities': ['light.kitchen', 'switch.ac'], @@ -358,7 +360,6 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture): 'exclude_entities': [], }, 'alexa_domains': ['switch'], - 'google_enabled': True, 'google_entities': { 'include_domains': ['light'], 'include_entities': [], @@ -505,8 +506,9 @@ 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): """Test updating preference.""" - assert setup_api[STORAGE_ENABLE_GOOGLE] - assert setup_api[STORAGE_ENABLE_ALEXA] + 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' @@ -517,9 +519,11 @@ async def test_websocket_update_preferences(hass, hass_ws_client, 'type': 'cloud/update_prefs', 'alexa_enabled': False, 'google_enabled': False, + 'google_allow_unlock': False, }) response = await client.receive_json() assert response['success'] - assert not setup_api[STORAGE_ENABLE_GOOGLE] - assert not setup_api[STORAGE_ENABLE_ALEXA] + assert not setup_api[PREF_ENABLE_GOOGLE] + assert not setup_api[PREF_ENABLE_ALEXA] + assert not setup_api[PREF_GOOGLE_ALLOW_UNLOCK] diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index d0b145c1b67..c900fc3a7a8 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -7,8 +7,9 @@ import pytest from homeassistant.setup import async_setup_component from homeassistant.components.cloud import ( - Cloud, iot, auth_api, MODE_DEV, STORAGE_ENABLE_ALEXA, - STORAGE_ENABLE_GOOGLE) + Cloud, iot, auth_api, MODE_DEV) +from homeassistant.components.cloud.const import ( + PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE) from tests.components.alexa import test_smart_home as test_alexa from tests.common import mock_coro @@ -308,7 +309,7 @@ def test_handler_alexa(hass): @asyncio.coroutine def test_handler_alexa_disabled(hass, mock_cloud_fixture): """Test handler Alexa when user has disabled it.""" - mock_cloud_fixture[STORAGE_ENABLE_ALEXA] = False + mock_cloud_fixture[PREF_ENABLE_ALEXA] = False resp = yield from iot.async_handle_alexa( hass, hass.data['cloud'], @@ -377,7 +378,7 @@ def test_handler_google_actions(hass): async def test_handler_google_actions_disabled(hass, mock_cloud_fixture): """Test handler Google Actions when user has disabled it.""" - mock_cloud_fixture[STORAGE_ENABLE_GOOGLE] = False + mock_cloud_fixture[PREF_ENABLE_GOOGLE] = False with patch('homeassistant.components.cloud.Cloud.async_start', return_value=mock_coro()):