mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
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:
parent
08591dae0e
commit
6c5124e12a
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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']))
|
||||||
|
@ -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."""
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user