Skip to content

Commit

Permalink
Add option to disable specific integrations (#16757)
Browse files Browse the repository at this point in the history
* Add option to disable specific integrations

* Lint
  • Loading branch information
balloob authored Sep 20, 2018
1 parent 03de658 commit 092c146
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 28 deletions.
5 changes: 5 additions & 0 deletions homeassistant/components/alexa/smart_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
47 changes: 43 additions & 4 deletions homeassistant/components/cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@
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 @@ -124,11 +129,13 @@ def __init__(self, hass, mode, alexa, google_actions,
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
Expand Down Expand Up @@ -193,6 +200,16 @@ def should_expose(entity):

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 @@ -231,10 +248,23 @@ def write_user_info(self):
'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:
Expand All @@ -255,7 +285,7 @@ def load_config():
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
Expand All @@ -274,6 +304,15 @@ 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)

@asyncio.coroutine
def _fetch_jwt_keyset(self):
"""Fetch the JWT keyset for the Cognito instance."""
Expand Down
34 changes: 34 additions & 0 deletions homeassistant/components/cloud/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
})


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,
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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,
}
6 changes: 6 additions & 0 deletions homeassistant/components/cloud/iot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/google_assistant/smart_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
}
31 changes: 31 additions & 0 deletions tests/components/cloud/__init__.py
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions tests/components/cloud/conftest.py
Original file line number Diff line number Diff line change
@@ -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)
58 changes: 39 additions & 19 deletions tests/components/cloud/test_http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
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'
Expand All @@ -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
Expand Down Expand Up @@ -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': '[email protected]',
Expand All @@ -338,6 +333,8 @@ async def test_websocket_status(hass, hass_ws_client):
'logged_in': True,
'email': '[email protected]',
'cloud': 'connected',
'alexa_enabled': True,
'google_enabled': True,
}


Expand Down Expand Up @@ -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': '[email protected]',
'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]
8 changes: 4 additions & 4 deletions tests/components/cloud/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand All @@ -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'
}
Expand Down
Loading

0 comments on commit 092c146

Please sign in to comment.