Cloud: allow managing Alexa entities via UI (#24522)

* Clean up Alexa config

* Cloud: Manage Alexa entities via UI

* Add tests for new cloud APIs
This commit is contained in:
Paulus Schoutsen 2019-06-13 11:58:08 -07:00 committed by GitHub
parent 08591dae0e
commit 6c5124e12a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 322 additions and 106 deletions

View File

@ -1,13 +1,33 @@
"""Config helpers for Alexa.""" """Config helpers for Alexa."""
class Config: class AbstractConfig:
"""Hold the configuration for Alexa.""" """Hold the configuration for Alexa."""
def __init__(self, endpoint, async_get_access_token, should_expose, @property
entity_config=None): def supports_auth(self):
"""Initialize the configuration.""" """Return if config supports auth."""
self.endpoint = endpoint return False
self.async_get_access_token = async_get_access_token
self.should_expose = should_expose @property
self.entity_config = entity_config or {} 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

View File

@ -48,8 +48,6 @@ API_CHANGE = 'change'
CONF_DESCRIPTION = 'description' CONF_DESCRIPTION = 'description'
CONF_DISPLAY_CATEGORIES = 'display_categories' CONF_DISPLAY_CATEGORIES = 'display_categories'
AUTH_KEY = "alexa.smart_home.auth"
API_TEMP_UNITS = { API_TEMP_UNITS = {
TEMP_FAHRENHEIT: 'FAHRENHEIT', TEMP_FAHRENHEIT: 'FAHRENHEIT',
TEMP_CELSIUS: 'CELSIUS', TEMP_CELSIUS: 'CELSIUS',

View File

@ -31,7 +31,6 @@ from homeassistant.components import cover, fan, group, light, media_player
from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.temperature import convert as convert_temperature
from .const import ( from .const import (
AUTH_KEY,
API_TEMP_UNITS, API_TEMP_UNITS,
API_THERMOSTAT_MODES, API_THERMOSTAT_MODES,
Cause, Cause,
@ -86,8 +85,8 @@ async def async_api_accept_grant(hass, config, directive, context):
auth_code = directive.payload['grant']['code'] auth_code = directive.payload['grant']['code']
_LOGGER.debug("AcceptGrant code: %s", auth_code) _LOGGER.debug("AcceptGrant code: %s", auth_code)
if AUTH_KEY in hass.data: if config.supports_auth:
await hass.data[AUTH_KEY].async_do_auth(auth_code) await config.async_accept_grant(auth_code)
await async_enable_proactive_mode(hass, config) await async_enable_proactive_mode(hass, config)
return directive.response( return directive.response(

View File

@ -5,9 +5,8 @@ from homeassistant import core
from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.view import HomeAssistantView
from .auth import Auth from .auth import Auth
from .config import Config from .config import AbstractConfig
from .const import ( from .const import (
AUTH_KEY,
CONF_CLIENT_ID, CONF_CLIENT_ID,
CONF_CLIENT_SECRET, CONF_CLIENT_SECRET,
CONF_ENDPOINT, CONF_ENDPOINT,
@ -21,6 +20,47 @@ _LOGGER = logging.getLogger(__name__)
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home' 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): async def async_setup(hass, config):
"""Activate Smart Home functionality of Alexa component. """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 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. by the cloud component which will call async_handle_message directly.
""" """
if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET): smart_home_config = AlexaConfig(hass, config)
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),
)
hass.http.register_view(SmartHomeView(smart_home_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) await async_enable_proactive_mode(hass, smart_home_config)

View File

@ -61,7 +61,6 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.Optional(CONF_MODE, default=DEFAULT_MODE):
vol.In([MODE_DEV, MODE_PROD]), vol.In([MODE_DEV, MODE_PROD]),
# Change to optional when we include real servers
vol.Optional(CONF_COGNITO_CLIENT_ID): str, vol.Optional(CONF_COGNITO_CLIENT_ID): str,
vol.Optional(CONF_USER_POOL_ID): str, vol.Optional(CONF_USER_POOL_ID): str,
vol.Optional(CONF_REGION): str, vol.Optional(CONF_REGION): str,

View File

@ -26,6 +26,38 @@ from .const import (
from .prefs import CloudPreferences 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): class CloudClient(Interface):
"""Interface class for Home Assistant Cloud.""" """Interface class for Home Assistant Cloud."""
@ -36,10 +68,10 @@ class CloudClient(Interface):
self._hass = hass self._hass = hass
self._prefs = prefs self._prefs = prefs
self._websession = websession 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 self._google_config = None
@property @property
@ -77,26 +109,11 @@ class CloudClient(Interface):
"""Return true if we want start a remote connection.""" """Return true if we want start a remote connection."""
return self._prefs.remote_enabled 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 @property
def google_config(self) -> ga_h.Config: def google_config(self) -> ga_h.Config:
"""Return Google config.""" """Return Google config."""
if not self._google_config: if not self._google_config:
google_conf = self._google_user_config google_conf = self.google_user_config
def should_expose(entity): def should_expose(entity):
"""If an entity should be exposed.""" """If an entity should be exposed."""
@ -134,14 +151,8 @@ class CloudClient(Interface):
return self._google_config 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: async def cleanups(self) -> None:
"""Cleanup some stuff after logout.""" """Cleanup some stuff after logout."""
self._alexa_config = None
self._google_config = None self._google_config = None
@callback @callback

View File

@ -9,6 +9,7 @@ PREF_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin'
PREF_CLOUDHOOKS = 'cloudhooks' PREF_CLOUDHOOKS = 'cloudhooks'
PREF_CLOUD_USER = 'cloud_user' PREF_CLOUD_USER = 'cloud_user'
PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs' PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs'
PREF_ALEXA_ENTITY_CONFIGS = 'alexa_entity_configs'
PREF_OVERRIDE_NAME = 'override_name' PREF_OVERRIDE_NAME = 'override_name'
PREF_DISABLE_2FA = 'disable_2fa' PREF_DISABLE_2FA = 'disable_2fa'
PREF_ALIASES = 'aliases' PREF_ALIASES = 'aliases'

View File

@ -90,6 +90,9 @@ async def async_setup(hass):
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
google_assistant_update) 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(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView) hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView) hass.http.register_view(CloudLogoutView)
@ -420,7 +423,7 @@ def _account_data(cloud):
'cloud': cloud.iot.state, 'cloud': cloud.iot.state,
'prefs': client.prefs.as_dict(), 'prefs': client.prefs.as_dict(),
'google_entities': client.google_user_config['filter'].config, '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), 'alexa_domains': list(alexa_entities.ENTITY_ADAPTERS),
'remote_domain': remote.instance_domain, 'remote_domain': remote.instance_domain,
'remote_connected': remote.is_connected, 'remote_connected': remote.is_connected,
@ -508,3 +511,52 @@ async def google_assistant_update(hass, connection, msg):
connection.send_result( connection.send_result(
msg['id'], msg['id'],
cloud.client.prefs.google_entity_configs.get(msg['entity_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']))

View File

@ -5,7 +5,7 @@ from .const import (
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER, PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA, 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) InvalidTrustedNetworks, InvalidTrustedProxies)
STORAGE_KEY = DOMAIN STORAGE_KEY = DOMAIN
@ -33,6 +33,7 @@ class CloudPreferences:
PREF_ENABLE_REMOTE: False, PREF_ENABLE_REMOTE: False,
PREF_GOOGLE_SECURE_DEVICES_PIN: None, PREF_GOOGLE_SECURE_DEVICES_PIN: None,
PREF_GOOGLE_ENTITY_CONFIGS: {}, PREF_GOOGLE_ENTITY_CONFIGS: {},
PREF_ALEXA_ENTITY_CONFIGS: {},
PREF_CLOUDHOOKS: {}, PREF_CLOUDHOOKS: {},
PREF_CLOUD_USER: None, PREF_CLOUD_USER: None,
} }
@ -42,7 +43,8 @@ class CloudPreferences:
async def async_update(self, *, google_enabled=_UNDEF, async def async_update(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF, remote_enabled=_UNDEF, alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
google_secure_devices_pin=_UNDEF, cloudhooks=_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.""" """Update user preferences."""
for key, value in ( for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled), (PREF_ENABLE_GOOGLE, google_enabled),
@ -52,6 +54,7 @@ class CloudPreferences:
(PREF_CLOUDHOOKS, cloudhooks), (PREF_CLOUDHOOKS, cloudhooks),
(PREF_CLOUD_USER, cloud_user), (PREF_CLOUD_USER, cloud_user),
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs), (PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
(PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs),
): ):
if value is not _UNDEF: if value is not _UNDEF:
self._prefs[key] = value self._prefs[key] = value
@ -95,6 +98,33 @@ class CloudPreferences:
} }
await self.async_update(google_entity_configs=updated_entities) 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): def as_dict(self):
"""Return dictionary version.""" """Return dictionary version."""
return { return {
@ -103,6 +133,7 @@ class CloudPreferences:
PREF_ENABLE_REMOTE: self.remote_enabled, PREF_ENABLE_REMOTE: self.remote_enabled,
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin, PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs, PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs,
PREF_CLOUDHOOKS: self.cloudhooks, PREF_CLOUDHOOKS: self.cloudhooks,
PREF_CLOUD_USER: self.cloud_user, PREF_CLOUD_USER: self.cloud_user,
} }
@ -140,6 +171,11 @@ class CloudPreferences:
"""Return Google Entity configurations.""" """Return Google Entity configurations."""
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {}) 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 @property
def cloudhooks(self): def cloudhooks(self):
"""Return the published cloud webhooks.""" """Return the published cloud webhooks."""

View File

@ -10,15 +10,35 @@ TEST_URL = "https://api.amazonalexa.com/v3/events"
TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token" TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token"
async def get_access_token(): class MockConfig(config.AbstractConfig):
"""Return a test access token.""" """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" return "thisisnotanacesstoken"
async def async_accept_grant(self, code):
"""Accept a grant."""
pass
DEFAULT_CONFIG = config.Config(
endpoint=TEST_URL, DEFAULT_CONFIG = MockConfig()
async_get_access_token=get_access_token,
should_expose=lambda entity_id: True)
def get_new_request(namespace, name, endpoint=None): def get_new_request(namespace, name, endpoint=None):

View File

@ -4,7 +4,6 @@ import pytest
from homeassistant.core import Context, callback from homeassistant.core import Context, callback
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.components.alexa import ( from homeassistant.components.alexa import (
config,
smart_home, smart_home,
messages, messages,
) )
@ -14,6 +13,7 @@ from tests.common import async_mock_service
from . import ( from . import (
get_new_request, get_new_request,
MockConfig,
DEFAULT_CONFIG, DEFAULT_CONFIG,
assert_request_calls_service, assert_request_calls_service,
assert_request_fails, assert_request_fails,
@ -1012,15 +1012,13 @@ async def test_exclude_filters(hass):
hass.states.async_set( hass.states.async_set(
'cover.deny', 'off', {'friendly_name': "Blocked cover"}) 'cover.deny', 'off', {'friendly_name': "Blocked cover"})
alexa_config = config.Config( alexa_config = MockConfig()
endpoint=None, alexa_config.should_expose = entityfilter.generate_filter(
async_get_access_token=None,
should_expose=entityfilter.generate_filter(
include_domains=[], include_domains=[],
include_entities=[], include_entities=[],
exclude_domains=['script'], exclude_domains=['script'],
exclude_entities=['cover.deny'], exclude_entities=['cover.deny'],
)) )
msg = await smart_home.async_handle_message(hass, alexa_config, request) msg = await smart_home.async_handle_message(hass, alexa_config, request)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1047,15 +1045,13 @@ async def test_include_filters(hass):
hass.states.async_set( hass.states.async_set(
'group.allow', 'off', {'friendly_name': "Allowed group"}) 'group.allow', 'off', {'friendly_name': "Allowed group"})
alexa_config = config.Config( alexa_config = MockConfig()
endpoint=None, alexa_config.should_expose = entityfilter.generate_filter(
async_get_access_token=None,
should_expose=entityfilter.generate_filter(
include_domains=['automation', 'group'], include_domains=['automation', 'group'],
include_entities=['script.deny'], include_entities=['script.deny'],
exclude_domains=[], exclude_domains=[],
exclude_entities=[], exclude_entities=[],
)) )
msg = await smart_home.async_handle_message(hass, alexa_config, request) msg = await smart_home.async_handle_message(hass, alexa_config, request)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1076,15 +1072,13 @@ async def test_never_exposed_entities(hass):
hass.states.async_set( hass.states.async_set(
'group.allow', 'off', {'friendly_name': "Allowed group"}) 'group.allow', 'off', {'friendly_name': "Allowed group"})
alexa_config = config.Config( alexa_config = MockConfig()
endpoint=None, alexa_config.should_expose = entityfilter.generate_filter(
async_get_access_token=None,
should_expose=entityfilter.generate_filter(
include_domains=['group'], include_domains=['group'],
include_entities=[], include_entities=[],
exclude_domains=[], exclude_domains=[],
exclude_entities=[], exclude_entities=[],
)) )
msg = await smart_home.async_handle_message(hass, alexa_config, request) msg = await smart_home.async_handle_message(hass, alexa_config, request)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1161,18 +1155,14 @@ async def test_entity_config(hass):
hass.states.async_set( hass.states.async_set(
'light.test_1', 'on', {'friendly_name': "Test light 1"}) 'light.test_1', 'on', {'friendly_name': "Test light 1"})
alexa_config = config.Config( alexa_config = MockConfig()
endpoint=None, alexa_config.entity_config = {
async_get_access_token=None,
should_expose=lambda entity_id: True,
entity_config={
'light.test_1': { 'light.test_1': {
'name': 'Config name', 'name': 'Config name',
'display_categories': 'SWITCH', 'display_categories': 'SWITCH',
'description': 'Config description' 'description': 'Config description'
} }
} }
)
msg = await smart_home.async_handle_message( msg = await smart_home.async_handle_message(
hass, alexa_config, request) hass, alexa_config, request)

View File

@ -7,7 +7,8 @@ import pytest
from homeassistant.core import State from homeassistant.core import State
from homeassistant.setup import async_setup_component 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 ( from homeassistant.components.cloud.const import (
PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE) PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE)
from tests.components.alexa import test_smart_home as test_alexa 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) 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')

View File

@ -15,6 +15,7 @@ from homeassistant.components.cloud.const import (
DOMAIN) DOMAIN)
from homeassistant.components.google_assistant.helpers import ( from homeassistant.components.google_assistant.helpers import (
GoogleEntity, Config) GoogleEntity, Config)
from homeassistant.components.alexa.entities import LightCapabilities
from tests.common import mock_coro 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_enabled': True,
'google_entity_configs': {}, 'google_entity_configs': {},
'google_secure_devices_pin': None, 'google_secure_devices_pin': None,
'alexa_entity_configs': {},
'remote_enabled': False, 'remote_enabled': False,
}, },
'alexa_entities': { '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.' 'Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.'
assert len(mock_connect.mock_calls) == 0 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,
}