Add option to disable specific integrations (#16757)

* Add option to disable specific integrations

* Lint
pull/16761/head
Paulus Schoutsen 2018-09-20 23:46:51 +02:00 committed by GitHub
parent 03de658d4d
commit 092c146eae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 222 additions and 28 deletions

View File

@ -1529,3 +1529,8 @@ async def async_api_reportstate(hass, config, request, context, entity):
name='StateReport',
context={'properties': properties}
)
def turned_off_response(message):
"""Return a device turned off response."""
return api_error(message[API_DIRECTIVE], error_type='BRIDGE_UNREACHABLE')

View File

@ -27,8 +27,13 @@ from . import http_api, iot, auth_api
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'
@ -124,11 +129,13 @@ class Cloud:
self.alexa_config = alexa
self._google_actions = google_actions
self._gactions_config = None
self._prefs = None
self.jwt_keyset = None
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
@ -193,6 +200,16 @@ class Cloud:
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.
@ -231,10 +248,23 @@ class Cloud:
'refresh_token': self.refresh_token,
}, indent=4))
@asyncio.coroutine
def async_start(self, _):
async def async_start(self, _):
"""Start the cloud component."""
success = yield from self._fetch_jwt_keyset()
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
success = await self._fetch_jwt_keyset()
# Fetching keyset can fail if internet is not up yet.
if not success:
@ -255,7 +285,7 @@ class Cloud:
with open(user_info, 'rt') as file:
return json.loads(file.read())
info = yield from self.hass.async_add_job(load_config)
info = await self.hass.async_add_job(load_config)
if info is None:
return
@ -274,6 +304,15 @@ 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)
@asyncio.coroutine
def _fetch_jwt_keyset(self):
"""Fetch the JWT keyset for the Cognito instance."""

View File

@ -25,6 +25,14 @@ 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,
})
WS_TYPE_SUBSCRIPTION = 'cloud/subscription'
SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SUBSCRIPTION,
@ -41,6 +49,10 @@ async def async_setup(hass):
WS_TYPE_SUBSCRIPTION, websocket_subscription,
SCHEMA_WS_SUBSCRIPTION
)
hass.components.websocket_api.async_register_command(
WS_TYPE_UPDATE_PREFS, websocket_update_prefs,
SCHEMA_WS_UPDATE_PREFS
)
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
@ -245,6 +257,26 @@ async def websocket_subscription(hass, connection, msg):
msg['id'], 'request_failed', 'Failed to request subscription'))
@websocket_api.async_response
async def websocket_update_prefs(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]
if not cloud.is_logged_in:
connection.to_write.put_nowait(websocket_api.error_message(
msg['id'], 'not_logged_in',
'You need to be logged in to the cloud.'))
return
changes = dict(msg)
changes.pop('id')
changes.pop('type')
await cloud.update_preferences(**changes)
connection.send_message_outside(websocket_api.result_message(
msg['id'], {'success': True}))
def _account_data(cloud):
"""Generate the auth data JSON response."""
if not cloud.is_logged_in:
@ -259,4 +291,6 @@ def _account_data(cloud):
'logged_in': True,
'email': claims['email'],
'cloud': cloud.iot.state,
'google_enabled': cloud.google_enabled,
'alexa_enabled': cloud.alexa_enabled,
}

View File

@ -227,6 +227,9 @@ def async_handle_message(hass, cloud, handler_name, payload):
@asyncio.coroutine
def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa."""
if not cloud.alexa_enabled:
return alexa.turned_off_response(payload)
result = yield from alexa.async_handle_message(
hass, cloud.alexa_config, payload)
return result
@ -236,6 +239,9 @@ 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:
return ga.turned_off_response(payload)
result = yield from ga.async_handle_message(
hass, cloud.gactions_config, payload)
return result

View File

@ -324,3 +324,11 @@ async def handle_devices_execute(hass, config, payload):
})
return {'commands': final_results}
def turned_off_response(message):
"""Return a device turned off response."""
return {
'requestId': message.get('requestId'),
'payload': {'errorCode': 'deviceTurnedOff'}
}

View File

@ -1 +1,32 @@
"""Tests for the cloud component."""
from unittest.mock import patch
from homeassistant.setup import async_setup_component
from homeassistant.components import cloud
from jose import jwt
from tests.common import mock_coro
def mock_cloud(hass, config={}):
"""Mock cloud."""
with patch('homeassistant.components.cloud.Cloud.async_start',
return_value=mock_coro()):
assert hass.loop.run_until_complete(async_setup_component(
hass, cloud.DOMAIN, {
'cloud': config
}))
hass.data[cloud.DOMAIN]._decode_claims = \
lambda token: jwt.get_unverified_claims(token)
def mock_cloud_prefs(hass, prefs={}):
"""Fixture for cloud component."""
prefs_to_set = {
cloud.STORAGE_ENABLE_ALEXA: True,
cloud.STORAGE_ENABLE_GOOGLE: True,
}
prefs_to_set.update(prefs)
hass.data[cloud.DOMAIN]._prefs = prefs_to_set
return prefs_to_set

View File

@ -0,0 +1,11 @@
"""Fixtures for cloud tests."""
import pytest
from . import mock_cloud, mock_cloud_prefs
@pytest.fixture
def mock_cloud_fixture(hass):
"""Fixture for cloud component."""
mock_cloud(hass)
return mock_cloud_prefs(hass)

View File

@ -5,11 +5,12 @@ from unittest.mock import patch, MagicMock
import pytest
from jose import jwt
from homeassistant.bootstrap import async_setup_component
from homeassistant.components.cloud import DOMAIN, auth_api, iot
from homeassistant.components.cloud import (
DOMAIN, auth_api, iot, STORAGE_ENABLE_GOOGLE, STORAGE_ENABLE_ALEXA)
from tests.common import mock_coro
from . import mock_cloud, mock_cloud_prefs
GOOGLE_ACTIONS_SYNC_URL = 'https://api-test.hass.io/google_actions_sync'
SUBSCRIPTION_INFO_URL = 'https://api-test.hass.io/subscription_info'
@ -25,22 +26,16 @@ def mock_auth():
@pytest.fixture(autouse=True)
def setup_api(hass):
"""Initialize HTTP API."""
with patch('homeassistant.components.cloud.Cloud.async_start',
return_value=mock_coro()):
assert hass.loop.run_until_complete(async_setup_component(
hass, 'cloud', {
'cloud': {
'mode': 'development',
'cognito_client_id': 'cognito_client_id',
'user_pool_id': 'user_pool_id',
'region': 'region',
'relayer': 'relayer',
'google_actions_sync_url': GOOGLE_ACTIONS_SYNC_URL,
'subscription_info_url': SUBSCRIPTION_INFO_URL,
}
}))
hass.data['cloud']._decode_claims = \
lambda token: jwt.get_unverified_claims(token)
mock_cloud(hass, {
'mode': 'development',
'cognito_client_id': 'cognito_client_id',
'user_pool_id': 'user_pool_id',
'region': 'region',
'relayer': 'relayer',
'google_actions_sync_url': GOOGLE_ACTIONS_SYNC_URL,
'subscription_info_url': SUBSCRIPTION_INFO_URL,
})
return mock_cloud_prefs(hass)
@pytest.fixture
@ -321,7 +316,7 @@ def test_resend_confirm_view_unknown_error(mock_cognito, cloud_client):
assert req.status == 502
async def test_websocket_status(hass, hass_ws_client):
async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture):
"""Test querying the status."""
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
@ -338,6 +333,8 @@ async def test_websocket_status(hass, hass_ws_client):
'logged_in': True,
'email': 'hello@home-assistant.io',
'cloud': 'connected',
'alexa_enabled': True,
'google_enabled': True,
}
@ -407,3 +404,26 @@ async def test_websocket_subscription_not_logged_in(hass, hass_ws_client):
assert not response['success']
assert response['error']['code'] == 'not_logged_in'
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]
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,
'type': 'cloud/update_prefs',
'alexa_enabled': False,
'google_enabled': False,
})
response = await client.receive_json()
assert response['success']
assert not setup_api[STORAGE_ENABLE_GOOGLE]
assert not setup_api[STORAGE_ENABLE_ALEXA]

View File

@ -141,9 +141,9 @@ def test_write_user_info():
@asyncio.coroutine
def test_subscription_expired():
def test_subscription_expired(hass):
"""Test subscription being expired."""
cl = cloud.Cloud(None, cloud.MODE_DEV, None, None)
cl = cloud.Cloud(hass, cloud.MODE_DEV, None, None)
token_val = {
'custom:sub-exp': '2017-11-13'
}
@ -154,9 +154,9 @@ def test_subscription_expired():
@asyncio.coroutine
def test_subscription_not_expired():
def test_subscription_not_expired(hass):
"""Test subscription not being expired."""
cl = cloud.Cloud(None, cloud.MODE_DEV, None, None)
cl = cloud.Cloud(hass, cloud.MODE_DEV, None, None)
token_val = {
'custom:sub-exp': '2017-11-13'
}

View File

@ -6,10 +6,14 @@ from aiohttp import WSMsgType, client_exceptions
import pytest
from homeassistant.setup import async_setup_component
from homeassistant.components.cloud import Cloud, iot, auth_api, MODE_DEV
from homeassistant.components.cloud import (
Cloud, iot, auth_api, MODE_DEV, STORAGE_ENABLE_ALEXA,
STORAGE_ENABLE_GOOGLE)
from tests.components.alexa import test_smart_home as test_alexa
from tests.common import mock_coro
from . import mock_cloud_prefs
@pytest.fixture
def mock_client():
@ -284,6 +288,8 @@ def test_handler_alexa(hass):
})
assert setup
mock_cloud_prefs(hass)
resp = yield from iot.async_handle_alexa(
hass, hass.data['cloud'],
test_alexa.get_new_request('Alexa.Discovery', 'Discover'))
@ -299,6 +305,20 @@ def test_handler_alexa(hass):
assert device['manufacturerName'] == 'Home Assistant'
@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
resp = yield from iot.async_handle_alexa(
hass, hass.data['cloud'],
test_alexa.get_new_request('Alexa.Discovery', 'Discover'))
assert resp['event']['header']['namespace'] == 'Alexa'
assert resp['event']['header']['name'] == 'ErrorResponse'
assert resp['event']['payload']['type'] == 'BRIDGE_UNREACHABLE'
@asyncio.coroutine
def test_handler_google_actions(hass):
"""Test handler Google Actions."""
@ -327,6 +347,8 @@ def test_handler_google_actions(hass):
})
assert setup
mock_cloud_prefs(hass)
reqid = '5711642932632160983'
data = {'requestId': reqid, 'inputs': [{'intent': 'action.devices.SYNC'}]}
@ -351,6 +373,24 @@ def test_handler_google_actions(hass):
assert device['roomHint'] == 'living room'
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
with patch('homeassistant.components.cloud.Cloud.async_start',
return_value=mock_coro()):
assert await async_setup_component(hass, 'cloud', {})
reqid = '5711642932632160983'
data = {'requestId': reqid, 'inputs': [{'intent': 'action.devices.SYNC'}]}
resp = await iot.async_handle_google_actions(
hass, hass.data['cloud'], data)
assert resp['requestId'] == reqid
assert resp['payload']['errorCode'] == 'deviceTurnedOff'
async def test_refresh_token_expired(hass):
"""Test handling Unauthenticated error raised if refresh token expired."""
cloud = Cloud(hass, MODE_DEV, None, None)