Skip to content

Commit

Permalink
Add cloud pref for Google unlock (#18600)
Browse files Browse the repository at this point in the history
  • Loading branch information
balloob committed Nov 20, 2018
1 parent b774299 commit d9c7f77
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 66 deletions.
49 changes: 5 additions & 44 deletions homeassistant/components/cloud/__init__.py
Expand Up @@ -20,17 +20,12 @@
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'
Expand Down Expand Up @@ -70,8 +65,6 @@

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({
Expand Down Expand Up @@ -127,12 +120,11 @@ def __init__(self, hass, mode, alexa, google_actions,
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
Expand Down Expand Up @@ -196,21 +188,11 @@ def should_expose(entity):
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.
Expand Down Expand Up @@ -250,20 +232,6 @@ def write_user_info(self):

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
Expand All @@ -280,6 +248,8 @@ def load_config():

info = await self.hass.async_add_job(load_config)

await self.prefs.async_initialize(not info)

if info is None:
return

Expand All @@ -289,15 +259,6 @@ def load_config():

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
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/cloud/const.py
Expand Up @@ -3,6 +3,10 @@
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',
Expand Down
14 changes: 8 additions & 6 deletions homeassistant/components/cloud/http_api.py
Expand Up @@ -15,7 +15,9 @@
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__)
Expand All @@ -30,8 +32,9 @@
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,
})


Expand Down Expand Up @@ -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}))
Expand All @@ -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),
}
4 changes: 2 additions & 2 deletions homeassistant/components/cloud/iot.py
Expand Up @@ -229,15 +229,15 @@ 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


@HANDLERS.register('google_actions')
@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(
Expand Down
63 changes: 63 additions & 0 deletions 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)
8 changes: 5 additions & 3 deletions tests/components/cloud/__init__.py
Expand Up @@ -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

Expand All @@ -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
18 changes: 11 additions & 7 deletions tests/components/cloud/test_http_api.py
Expand Up @@ -6,7 +6,9 @@
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
Expand Down Expand Up @@ -350,15 +352,14 @@ 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'],
'exclude_domains': [],
'exclude_entities': [],
},
'alexa_domains': ['switch'],
'google_enabled': True,
'google_entities': {
'include_domains': ['light'],
'include_entities': [],
Expand Down Expand Up @@ -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'
Expand All @@ -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]
9 changes: 5 additions & 4 deletions tests/components/cloud/test_iot.py
Expand Up @@ -7,8 +7,9 @@

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

Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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()):
Expand Down

0 comments on commit d9c7f77

Please sign in to comment.