Add option to disable specific integrations (#16757)
* Add option to disable specific integrations * Lintpull/16761/head
parent
03de658d4d
commit
092c146eae
|
@ -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')
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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]
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue