From 6c5124e12a6dae2b179539a2466e81b1edce35c0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Jun 2019 11:58:08 -0700 Subject: [PATCH] Cloud: allow managing Alexa entities via UI (#24522) * Clean up Alexa config * Cloud: Manage Alexa entities via UI * Add tests for new cloud APIs --- homeassistant/components/alexa/config.py | 36 +++++++--- homeassistant/components/alexa/const.py | 2 - homeassistant/components/alexa/handlers.py | 5 +- .../components/alexa/smart_home_http.py | 61 ++++++++++++----- homeassistant/components/cloud/__init__.py | 1 - homeassistant/components/cloud/client.py | 61 ++++++++++------- homeassistant/components/cloud/const.py | 1 + homeassistant/components/cloud/http_api.py | 54 ++++++++++++++- homeassistant/components/cloud/prefs.py | 40 ++++++++++- tests/components/alexa/__init__.py | 34 ++++++++-- tests/components/alexa/test_smart_home.py | 68 ++++++++----------- tests/components/cloud/test_client.py | 20 +++++- tests/components/cloud/test_http_api.py | 45 ++++++++++++ 13 files changed, 322 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index df9c9b013dc..b5060709ce3 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -1,13 +1,33 @@ """Config helpers for Alexa.""" -class Config: +class AbstractConfig: """Hold the configuration for Alexa.""" - def __init__(self, endpoint, async_get_access_token, should_expose, - entity_config=None): - """Initialize the configuration.""" - self.endpoint = endpoint - self.async_get_access_token = async_get_access_token - self.should_expose = should_expose - self.entity_config = entity_config or {} + @property + def supports_auth(self): + """Return if config supports auth.""" + return False + + @property + def endpoint(self): + """Endpoint for report state.""" + return None + + @property + def entity_config(self): + """Return entity config.""" + return {} + + def should_expose(self, entity_id): + """If an entity should be exposed.""" + # pylint: disable=no-self-use + return False + + async def async_get_access_token(self): + """Get an access token.""" + raise NotImplementedError + + async def async_accept_grant(self, code): + """Accept a grant.""" + raise NotImplementedError diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 9931406ff0e..513c4ac43d7 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -48,8 +48,6 @@ API_CHANGE = 'change' CONF_DESCRIPTION = 'description' CONF_DISPLAY_CATEGORIES = 'display_categories' -AUTH_KEY = "alexa.smart_home.auth" - API_TEMP_UNITS = { TEMP_FAHRENHEIT: 'FAHRENHEIT', TEMP_CELSIUS: 'CELSIUS', diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index a17381b3e17..5a1a899ea69 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -31,7 +31,6 @@ from homeassistant.components import cover, fan, group, light, media_player from homeassistant.util.temperature import convert as convert_temperature from .const import ( - AUTH_KEY, API_TEMP_UNITS, API_THERMOSTAT_MODES, Cause, @@ -86,8 +85,8 @@ async def async_api_accept_grant(hass, config, directive, context): auth_code = directive.payload['grant']['code'] _LOGGER.debug("AcceptGrant code: %s", auth_code) - if AUTH_KEY in hass.data: - await hass.data[AUTH_KEY].async_do_auth(auth_code) + if config.supports_auth: + await config.async_accept_grant(auth_code) await async_enable_proactive_mode(hass, config) return directive.response( diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index cb70fb86253..d0c4429e6b2 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -5,9 +5,8 @@ from homeassistant import core from homeassistant.components.http.view import HomeAssistantView from .auth import Auth -from .config import Config +from .config import AbstractConfig from .const import ( - AUTH_KEY, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_ENDPOINT, @@ -21,6 +20,47 @@ _LOGGER = logging.getLogger(__name__) SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home' +class AlexaConfig(AbstractConfig): + """Alexa config.""" + + def __init__(self, hass, config): + """Initialize Alexa config.""" + self._config = config + + if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET): + self._auth = Auth(hass, config[CONF_CLIENT_ID], + config[CONF_CLIENT_SECRET]) + else: + self._auth = None + + @property + def supports_auth(self): + """Return if config supports auth.""" + return self._auth is not None + + @property + def endpoint(self): + """Endpoint for report state.""" + return self._config.get(CONF_ENDPOINT) + + @property + def entity_config(self): + """Return entity config.""" + return self._config.get(CONF_ENTITY_CONFIG, {}) + + def should_expose(self, entity_id): + """If an entity should be exposed.""" + return self._config[CONF_FILTER](entity_id) + + async def async_get_access_token(self): + """Get an access token.""" + return await self._auth.async_get_access_token() + + async def async_accept_grant(self, code): + """Accept a grant.""" + return await self._auth.async_do_auth(code) + + async def async_setup(hass, config): """Activate Smart Home functionality of Alexa component. @@ -30,23 +70,10 @@ async def async_setup(hass, config): Even if that's disabled, the functionality in this module may still be used by the cloud component which will call async_handle_message directly. """ - if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET): - hass.data[AUTH_KEY] = Auth(hass, config[CONF_CLIENT_ID], - config[CONF_CLIENT_SECRET]) - - async_get_access_token = \ - hass.data[AUTH_KEY].async_get_access_token if AUTH_KEY in hass.data \ - else None - - smart_home_config = Config( - endpoint=config.get(CONF_ENDPOINT), - async_get_access_token=async_get_access_token, - should_expose=config[CONF_FILTER], - entity_config=config.get(CONF_ENTITY_CONFIG), - ) + smart_home_config = AlexaConfig(hass, config) hass.http.register_view(SmartHomeView(smart_home_config)) - if AUTH_KEY in hass.data: + if smart_home_config.supports_auth: await async_enable_proactive_mode(hass, smart_home_config) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 5490a0da156..01e2b48559b 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -61,7 +61,6 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In([MODE_DEV, MODE_PROD]), - # Change to optional when we include real servers vol.Optional(CONF_COGNITO_CLIENT_ID): str, vol.Optional(CONF_USER_POOL_ID): str, vol.Optional(CONF_REGION): str, diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index f5edefeee43..f6d283ee1eb 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -26,6 +26,38 @@ from .const import ( from .prefs import CloudPreferences +class AlexaConfig(alexa_config.AbstractConfig): + """Alexa Configuration.""" + + def __init__(self, config, prefs): + """Initialize the Alexa config.""" + self._config = config + self._prefs = prefs + + @property + def endpoint(self): + """Endpoint for report state.""" + return None + + @property + def entity_config(self): + """Return entity config.""" + return self._config.get(CONF_ENTITY_CONFIG, {}) + + def should_expose(self, entity_id): + """If an entity should be exposed.""" + if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + return False + + if not self._config[CONF_FILTER].empty_filter: + return self._config[CONF_FILTER](entity_id) + + entity_configs = self._prefs.alexa_entity_configs + entity_config = entity_configs.get(entity_id, {}) + return entity_config.get( + PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) + + class CloudClient(Interface): """Interface class for Home Assistant Cloud.""" @@ -36,10 +68,10 @@ class CloudClient(Interface): self._hass = hass self._prefs = prefs self._websession = websession - self._alexa_user_config = alexa_cfg - self._google_user_config = google_config + self.google_user_config = google_config + self.alexa_user_config = alexa_cfg - self._alexa_config = None + self.alexa_config = AlexaConfig(alexa_cfg, prefs) self._google_config = None @property @@ -77,26 +109,11 @@ class CloudClient(Interface): """Return true if we want start a remote connection.""" return self._prefs.remote_enabled - @property - def alexa_config(self) -> alexa_config.Config: - """Return Alexa config.""" - if not self._alexa_config: - alexa_conf = self._alexa_user_config - - self._alexa_config = alexa_config.Config( - endpoint=None, - async_get_access_token=None, - should_expose=alexa_conf[CONF_FILTER], - entity_config=alexa_conf.get(CONF_ENTITY_CONFIG), - ) - - return self._alexa_config - @property def google_config(self) -> ga_h.Config: """Return Google config.""" if not self._google_config: - google_conf = self._google_user_config + google_conf = self.google_user_config def should_expose(entity): """If an entity should be exposed.""" @@ -134,14 +151,8 @@ class CloudClient(Interface): return self._google_config - @property - def google_user_config(self) -> Dict[str, Any]: - """Return google action user config.""" - return self._google_user_config - async def cleanups(self) -> None: """Cleanup some stuff after logout.""" - self._alexa_config = None self._google_config = None @callback diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 65062213a63..505232bfb85 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -9,6 +9,7 @@ PREF_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin' PREF_CLOUDHOOKS = 'cloudhooks' PREF_CLOUD_USER = 'cloud_user' PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs' +PREF_ALEXA_ENTITY_CONFIGS = 'alexa_entity_configs' PREF_OVERRIDE_NAME = 'override_name' PREF_DISABLE_2FA = 'disable_2fa' PREF_ALIASES = 'aliases' diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 9c167d25601..eb3b0565351 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -90,6 +90,9 @@ async def async_setup(hass): hass.components.websocket_api.async_register_command( google_assistant_update) + hass.components.websocket_api.async_register_command(alexa_list) + hass.components.websocket_api.async_register_command(alexa_update) + hass.http.register_view(GoogleActionsSyncView) hass.http.register_view(CloudLoginView) hass.http.register_view(CloudLogoutView) @@ -420,7 +423,7 @@ def _account_data(cloud): 'cloud': cloud.iot.state, 'prefs': client.prefs.as_dict(), 'google_entities': client.google_user_config['filter'].config, - 'alexa_entities': client.alexa_config.should_expose.config, + 'alexa_entities': client.alexa_user_config['filter'].config, 'alexa_domains': list(alexa_entities.ENTITY_ADAPTERS), 'remote_domain': remote.instance_domain, 'remote_connected': remote.is_connected, @@ -508,3 +511,52 @@ async def google_assistant_update(hass, connection, msg): connection.send_result( msg['id'], cloud.client.prefs.google_entity_configs.get(msg['entity_id'])) + + +@websocket_api.require_admin +@_require_cloud_login +@websocket_api.async_response +@_ws_handle_cloud_errors +@websocket_api.websocket_command({ + 'type': 'cloud/alexa/entities' +}) +async def alexa_list(hass, connection, msg): + """List all alexa entities.""" + cloud = hass.data[DOMAIN] + entities = alexa_entities.async_get_entities( + hass, cloud.client.alexa_config + ) + + result = [] + + for entity in entities: + result.append({ + 'entity_id': entity.entity_id, + 'display_categories': entity.default_display_categories(), + 'interfaces': [ifc.name() for ifc in entity.interfaces()], + }) + + connection.send_result(msg['id'], result) + + +@websocket_api.require_admin +@_require_cloud_login +@websocket_api.async_response +@_ws_handle_cloud_errors +@websocket_api.websocket_command({ + 'type': 'cloud/alexa/entities/update', + 'entity_id': str, + vol.Optional('should_expose'): bool, +}) +async def alexa_update(hass, connection, msg): + """Update alexa entity config.""" + cloud = hass.data[DOMAIN] + changes = dict(msg) + changes.pop('type') + changes.pop('id') + + await cloud.client.prefs.async_update_alexa_entity_config(**changes) + + connection.send_result( + msg['id'], + cloud.client.prefs.alexa_entity_configs.get(msg['entity_id'])) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 9f2579134e5..1e4ac754460 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -5,7 +5,7 @@ from .const import ( DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER, PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA, - PREF_ALIASES, PREF_SHOULD_EXPOSE, + PREF_ALIASES, PREF_SHOULD_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS, InvalidTrustedNetworks, InvalidTrustedProxies) STORAGE_KEY = DOMAIN @@ -33,6 +33,7 @@ class CloudPreferences: PREF_ENABLE_REMOTE: False, PREF_GOOGLE_SECURE_DEVICES_PIN: None, PREF_GOOGLE_ENTITY_CONFIGS: {}, + PREF_ALEXA_ENTITY_CONFIGS: {}, PREF_CLOUDHOOKS: {}, PREF_CLOUD_USER: None, } @@ -42,7 +43,8 @@ class CloudPreferences: async def async_update(self, *, google_enabled=_UNDEF, alexa_enabled=_UNDEF, remote_enabled=_UNDEF, google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF, - cloud_user=_UNDEF, google_entity_configs=_UNDEF): + cloud_user=_UNDEF, google_entity_configs=_UNDEF, + alexa_entity_configs=_UNDEF): """Update user preferences.""" for key, value in ( (PREF_ENABLE_GOOGLE, google_enabled), @@ -52,6 +54,7 @@ class CloudPreferences: (PREF_CLOUDHOOKS, cloudhooks), (PREF_CLOUD_USER, cloud_user), (PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs), + (PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs), ): if value is not _UNDEF: self._prefs[key] = value @@ -95,6 +98,33 @@ class CloudPreferences: } await self.async_update(google_entity_configs=updated_entities) + async def async_update_alexa_entity_config( + self, *, entity_id, should_expose=_UNDEF): + """Update config for an Alexa entity.""" + entities = self.alexa_entity_configs + entity = entities.get(entity_id, {}) + + changes = {} + for key, value in ( + (PREF_SHOULD_EXPOSE, should_expose), + ): + if value is not _UNDEF: + changes[key] = value + + if not changes: + return + + updated_entity = { + **entity, + **changes, + } + + updated_entities = { + **entities, + entity_id: updated_entity, + } + await self.async_update(alexa_entity_configs=updated_entities) + def as_dict(self): """Return dictionary version.""" return { @@ -103,6 +133,7 @@ class CloudPreferences: PREF_ENABLE_REMOTE: self.remote_enabled, PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin, PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs, + PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs, PREF_CLOUDHOOKS: self.cloudhooks, PREF_CLOUD_USER: self.cloud_user, } @@ -140,6 +171,11 @@ class CloudPreferences: """Return Google Entity configurations.""" return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {}) + @property + def alexa_entity_configs(self): + """Return Alexa Entity configurations.""" + return self._prefs.get(PREF_ALEXA_ENTITY_CONFIGS, {}) + @property def cloudhooks(self): """Return the published cloud webhooks.""" diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index b1c8c6aa8bd..ab273d5e024 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -10,15 +10,35 @@ TEST_URL = "https://api.amazonalexa.com/v3/events" TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token" -async def get_access_token(): - """Return a test access token.""" - return "thisisnotanacesstoken" +class MockConfig(config.AbstractConfig): + """Mock Alexa config.""" + + entity_config = {} + + @property + def supports_auth(self): + """Return if config supports auth.""" + return True + + @property + def endpoint(self): + """Endpoint for report state.""" + return TEST_URL + + def should_expose(self, entity_id): + """If an entity should be exposed.""" + return True + + async def async_get_access_token(self): + """Get an access token.""" + return "thisisnotanacesstoken" + + async def async_accept_grant(self, code): + """Accept a grant.""" + pass -DEFAULT_CONFIG = config.Config( - endpoint=TEST_URL, - async_get_access_token=get_access_token, - should_expose=lambda entity_id: True) +DEFAULT_CONFIG = MockConfig() def get_new_request(namespace, name, endpoint=None): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index da7063f8acd..3aa1c7df366 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -4,7 +4,6 @@ import pytest from homeassistant.core import Context, callback from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.components.alexa import ( - config, smart_home, messages, ) @@ -14,6 +13,7 @@ from tests.common import async_mock_service from . import ( get_new_request, + MockConfig, DEFAULT_CONFIG, assert_request_calls_service, assert_request_fails, @@ -1012,15 +1012,13 @@ async def test_exclude_filters(hass): hass.states.async_set( 'cover.deny', 'off', {'friendly_name': "Blocked cover"}) - alexa_config = config.Config( - endpoint=None, - async_get_access_token=None, - should_expose=entityfilter.generate_filter( - include_domains=[], - include_entities=[], - exclude_domains=['script'], - exclude_entities=['cover.deny'], - )) + alexa_config = MockConfig() + alexa_config.should_expose = entityfilter.generate_filter( + include_domains=[], + include_entities=[], + exclude_domains=['script'], + exclude_entities=['cover.deny'], + ) msg = await smart_home.async_handle_message(hass, alexa_config, request) await hass.async_block_till_done() @@ -1047,15 +1045,13 @@ async def test_include_filters(hass): hass.states.async_set( 'group.allow', 'off', {'friendly_name': "Allowed group"}) - alexa_config = config.Config( - endpoint=None, - async_get_access_token=None, - should_expose=entityfilter.generate_filter( - include_domains=['automation', 'group'], - include_entities=['script.deny'], - exclude_domains=[], - exclude_entities=[], - )) + alexa_config = MockConfig() + alexa_config.should_expose = entityfilter.generate_filter( + include_domains=['automation', 'group'], + include_entities=['script.deny'], + exclude_domains=[], + exclude_entities=[], + ) msg = await smart_home.async_handle_message(hass, alexa_config, request) await hass.async_block_till_done() @@ -1076,15 +1072,13 @@ async def test_never_exposed_entities(hass): hass.states.async_set( 'group.allow', 'off', {'friendly_name': "Allowed group"}) - alexa_config = config.Config( - endpoint=None, - async_get_access_token=None, - should_expose=entityfilter.generate_filter( - include_domains=['group'], - include_entities=[], - exclude_domains=[], - exclude_entities=[], - )) + alexa_config = MockConfig() + alexa_config.should_expose = entityfilter.generate_filter( + include_domains=['group'], + include_entities=[], + exclude_domains=[], + exclude_entities=[], + ) msg = await smart_home.async_handle_message(hass, alexa_config, request) await hass.async_block_till_done() @@ -1161,18 +1155,14 @@ async def test_entity_config(hass): hass.states.async_set( 'light.test_1', 'on', {'friendly_name': "Test light 1"}) - alexa_config = config.Config( - endpoint=None, - async_get_access_token=None, - should_expose=lambda entity_id: True, - entity_config={ - 'light.test_1': { - 'name': 'Config name', - 'display_categories': 'SWITCH', - 'description': 'Config description' - } + alexa_config = MockConfig() + alexa_config.entity_config = { + 'light.test_1': { + 'name': 'Config name', + 'display_categories': 'SWITCH', + 'description': 'Config description' } - ) + } msg = await smart_home.async_handle_message( hass, alexa_config, request) diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index fa1d8cf8b9b..ca82d1e0aba 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -7,7 +7,8 @@ import pytest from homeassistant.core import State from homeassistant.setup import async_setup_component -from homeassistant.components.cloud import DOMAIN +from homeassistant.components.cloud import ( + DOMAIN, ALEXA_SCHEMA, prefs, client) from homeassistant.components.cloud.const import ( PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE) from tests.components.alexa import test_smart_home as test_alexa @@ -251,3 +252,20 @@ async def test_google_config_should_2fa( ) assert not cloud_client.google_config.should_2fa(state) + + +async def test_alexa_config_expose_entity_prefs(hass): + """Test Alexa config should expose using prefs.""" + cloud_prefs = prefs.CloudPreferences(hass) + await cloud_prefs.async_initialize() + entity_conf = { + 'should_expose': False + } + await cloud_prefs.async_update(alexa_entity_configs={ + 'light.kitchen': entity_conf + }) + conf = client.AlexaConfig(ALEXA_SCHEMA({}), cloud_prefs) + + assert not conf.should_expose('light.kitchen') + entity_conf['should_expose'] = True + assert conf.should_expose('light.kitchen') diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 68cd7fab891..0e4d46672ba 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -15,6 +15,7 @@ from homeassistant.components.cloud.const import ( DOMAIN) from homeassistant.components.google_assistant.helpers import ( GoogleEntity, Config) +from homeassistant.components.alexa.entities import LightCapabilities from tests.common import mock_coro @@ -361,6 +362,7 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture, 'google_enabled': True, 'google_entity_configs': {}, 'google_secure_devices_pin': None, + 'alexa_entity_configs': {}, 'remote_enabled': False, }, 'alexa_entities': { @@ -800,3 +802,46 @@ async def test_enabling_remote_trusted_proxies_local6( 'Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.' assert len(mock_connect.mock_calls) == 0 + + +async def test_list_alexa_entities( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test that we can list Alexa entities.""" + client = await hass_ws_client(hass) + entity = LightCapabilities(hass, MagicMock(entity_config={}), State( + 'light.kitchen', 'on' + )) + with patch('homeassistant.components.alexa.entities' + '.async_get_entities', return_value=[entity]): + await client.send_json({ + 'id': 5, + 'type': 'cloud/alexa/entities', + }) + response = await client.receive_json() + + assert response['success'] + assert len(response['result']) == 1 + assert response['result'][0] == { + 'entity_id': 'light.kitchen', + 'display_categories': ['LIGHT'], + 'interfaces': ['Alexa.PowerController', 'Alexa.EndpointHealth'], + } + + +async def test_update_alexa_entity( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test that we can update config of an Alexa entity.""" + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 5, + 'type': 'cloud/alexa/entities/update', + 'entity_id': 'light.kitchen', + 'should_expose': False, + }) + response = await client.receive_json() + + assert response['success'] + prefs = hass.data[DOMAIN].client.prefs + assert prefs.alexa_entity_configs['light.kitchen'] == { + 'should_expose': False, + }