Alexa to not use customize for entity config (#11461)
* Alexa to not use customize for entity config * Test Alexa entity config * Improve tests * Fix test
This commit is contained in:
parent
71fb7a6ef6
commit
8b57777ce9
5 changed files with 140 additions and 57 deletions
|
@ -1,6 +1,5 @@
|
|||
"""Support for alexa Smart Home Skill API."""
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import math
|
||||
from uuid import uuid4
|
||||
|
@ -27,10 +26,9 @@ API_EVENT = 'event'
|
|||
API_HEADER = 'header'
|
||||
API_PAYLOAD = 'payload'
|
||||
|
||||
ATTR_ALEXA_DESCRIPTION = 'alexa_description'
|
||||
ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories'
|
||||
ATTR_ALEXA_HIDDEN = 'alexa_hidden'
|
||||
ATTR_ALEXA_NAME = 'alexa_name'
|
||||
CONF_DESCRIPTION = 'description'
|
||||
CONF_DISPLAY_CATEGORIES = 'display_categories'
|
||||
CONF_NAME = 'name'
|
||||
|
||||
|
||||
MAPPING_COMPONENT = {
|
||||
|
@ -73,7 +71,13 @@ MAPPING_COMPONENT = {
|
|||
}
|
||||
|
||||
|
||||
Config = namedtuple('AlexaConfig', 'filter')
|
||||
class Config:
|
||||
"""Hold the configuration for Alexa."""
|
||||
|
||||
def __init__(self, should_expose, entity_config=None):
|
||||
"""Initialize the configuration."""
|
||||
self.should_expose = should_expose
|
||||
self.entity_config = entity_config or {}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -150,32 +154,28 @@ def async_api_discovery(hass, config, request):
|
|||
discovery_endpoints = []
|
||||
|
||||
for entity in hass.states.async_all():
|
||||
if not config.filter(entity.entity_id):
|
||||
if not config.should_expose(entity.entity_id):
|
||||
_LOGGER.debug("Not exposing %s because filtered by config",
|
||||
entity.entity_id)
|
||||
continue
|
||||
|
||||
if entity.attributes.get(ATTR_ALEXA_HIDDEN, False):
|
||||
_LOGGER.debug("Not exposing %s because alexa_hidden is true",
|
||||
entity.entity_id)
|
||||
continue
|
||||
|
||||
class_data = MAPPING_COMPONENT.get(entity.domain)
|
||||
|
||||
if not class_data:
|
||||
continue
|
||||
|
||||
friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name)
|
||||
description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION,
|
||||
entity.entity_id)
|
||||
entity_conf = config.entity_config.get(entity.entity_id, {})
|
||||
|
||||
friendly_name = entity_conf.get(CONF_NAME, entity.name)
|
||||
description = entity_conf.get(CONF_DESCRIPTION, entity.entity_id)
|
||||
|
||||
# Required description as per Amazon Scene docs
|
||||
if entity.domain == scene.DOMAIN:
|
||||
scene_fmt = '{} (Scene connected via Home Assistant)'
|
||||
description = scene_fmt.format(description)
|
||||
|
||||
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES
|
||||
display_categories = entity.attributes.get(cat_key, class_data[0])
|
||||
display_categories = entity_conf.get(CONF_DISPLAY_CATEGORIES,
|
||||
class_data[0])
|
||||
|
||||
endpoint = {
|
||||
'displayCategories': [display_categories],
|
||||
|
|
|
@ -12,6 +12,7 @@ import voluptuous as vol
|
|||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE)
|
||||
from homeassistant.helpers import entityfilter
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||
|
@ -25,7 +26,7 @@ REQUIREMENTS = ['warrant==0.6.1']
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ALEXA = 'alexa'
|
||||
CONF_GOOGLE_ASSISTANT = 'google_assistant'
|
||||
CONF_GOOGLE_ACTIONS = 'google_actions'
|
||||
CONF_FILTER = 'filter'
|
||||
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
|
||||
CONF_RELAYER = 'relayer'
|
||||
|
@ -35,6 +36,14 @@ MODE_DEV = 'development'
|
|||
DEFAULT_MODE = 'production'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
CONF_ENTITY_CONFIG = 'entity_config'
|
||||
|
||||
ALEXA_ENTITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(alexa_sh.CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(alexa_sh.CONF_DISPLAY_CATEGORIES): cv.string,
|
||||
vol.Optional(alexa_sh.CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
ASSISTANT_SCHEMA = vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_FILTER,
|
||||
|
@ -42,6 +51,10 @@ ASSISTANT_SCHEMA = vol.Schema({
|
|||
): entityfilter.FILTER_SCHEMA,
|
||||
})
|
||||
|
||||
ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
|
||||
|
@ -51,8 +64,8 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Optional(CONF_USER_POOL_ID): str,
|
||||
vol.Optional(CONF_REGION): str,
|
||||
vol.Optional(CONF_RELAYER): str,
|
||||
vol.Optional(CONF_ALEXA): ASSISTANT_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ASSISTANT): ASSISTANT_SCHEMA,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS): ASSISTANT_SCHEMA,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -61,18 +74,19 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
def async_setup(hass, config):
|
||||
"""Initialize the Home Assistant cloud."""
|
||||
if DOMAIN in config:
|
||||
kwargs = config[DOMAIN]
|
||||
kwargs = dict(config[DOMAIN])
|
||||
else:
|
||||
kwargs = {CONF_MODE: DEFAULT_MODE}
|
||||
|
||||
if CONF_ALEXA not in kwargs:
|
||||
kwargs[CONF_ALEXA] = ASSISTANT_SCHEMA({})
|
||||
alexa_conf = kwargs.pop(CONF_ALEXA, None) or ALEXA_SCHEMA({})
|
||||
gactions_conf = (kwargs.pop(CONF_GOOGLE_ACTIONS, None) or
|
||||
ASSISTANT_SCHEMA({}))
|
||||
|
||||
if CONF_GOOGLE_ASSISTANT not in kwargs:
|
||||
kwargs[CONF_GOOGLE_ASSISTANT] = ASSISTANT_SCHEMA({})
|
||||
|
||||
kwargs[CONF_ALEXA] = alexa_sh.Config(**kwargs[CONF_ALEXA])
|
||||
kwargs['gass_should_expose'] = kwargs.pop(CONF_GOOGLE_ASSISTANT)['filter']
|
||||
kwargs[CONF_ALEXA] = alexa_sh.Config(
|
||||
should_expose=alexa_conf[CONF_FILTER],
|
||||
entity_config=alexa_conf.get(CONF_ENTITY_CONFIG),
|
||||
)
|
||||
kwargs['gactions_should_expose'] = gactions_conf[CONF_FILTER]
|
||||
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
|
||||
|
||||
success = yield from cloud.initialize()
|
||||
|
@ -87,15 +101,15 @@ def async_setup(hass, config):
|
|||
class Cloud:
|
||||
"""Store the configuration of the cloud connection."""
|
||||
|
||||
def __init__(self, hass, mode, alexa, gass_should_expose,
|
||||
def __init__(self, hass, mode, alexa, gactions_should_expose,
|
||||
cognito_client_id=None, user_pool_id=None, region=None,
|
||||
relayer=None):
|
||||
"""Create an instance of Cloud."""
|
||||
self.hass = hass
|
||||
self.mode = mode
|
||||
self.alexa_config = alexa
|
||||
self._gass_should_expose = gass_should_expose
|
||||
self._gass_config = None
|
||||
self._gactions_should_expose = gactions_should_expose
|
||||
self._gactions_config = None
|
||||
self.jwt_keyset = None
|
||||
self.id_token = None
|
||||
self.access_token = None
|
||||
|
@ -144,15 +158,19 @@ class Cloud:
|
|||
return self.path('{}_auth.json'.format(self.mode))
|
||||
|
||||
@property
|
||||
def gass_config(self):
|
||||
def gactions_config(self):
|
||||
"""Return the Google Assistant config."""
|
||||
if self._gass_config is None:
|
||||
self._gass_config = ga_sh.Config(
|
||||
should_expose=self._gass_should_expose,
|
||||
if self._gactions_config is None:
|
||||
def should_expose(entity):
|
||||
"""If an entity should be exposed."""
|
||||
return self._gactions_should_expose(entity.entity_id)
|
||||
|
||||
self._gactions_config = ga_sh.Config(
|
||||
should_expose=should_expose,
|
||||
agent_user_id=self.claims['cognito:username']
|
||||
)
|
||||
|
||||
return self._gass_config
|
||||
return self._gactions_config
|
||||
|
||||
@asyncio.coroutine
|
||||
def initialize(self):
|
||||
|
@ -182,7 +200,7 @@ class Cloud:
|
|||
self.id_token = None
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
self._gass_config = None
|
||||
self._gactions_config = None
|
||||
|
||||
yield from self.hass.async_add_job(
|
||||
lambda: os.remove(self.user_info_path))
|
||||
|
|
|
@ -214,7 +214,7 @@ 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."""
|
||||
result = yield from ga.async_handle_message(hass, cloud.gass_config,
|
||||
result = yield from ga.async_handle_message(hass, cloud.gactions_config,
|
||||
payload)
|
||||
return result
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from homeassistant.helpers import entityfilter
|
|||
|
||||
from tests.common import async_mock_service
|
||||
|
||||
DEFAULT_CONFIG = smart_home.Config(filter=lambda entity_id: True)
|
||||
DEFAULT_CONFIG = smart_home.Config(should_expose=lambda entity_id: True)
|
||||
|
||||
|
||||
def get_new_request(namespace, name, endpoint=None):
|
||||
|
@ -338,7 +338,7 @@ def test_exclude_filters(hass):
|
|||
hass.states.async_set(
|
||||
'cover.deny', 'off', {'friendly_name': "Blocked cover"})
|
||||
|
||||
config = smart_home.Config(filter=entityfilter.generate_filter(
|
||||
config = smart_home.Config(should_expose=entityfilter.generate_filter(
|
||||
include_domains=[],
|
||||
include_entities=[],
|
||||
exclude_domains=['script'],
|
||||
|
@ -371,7 +371,7 @@ def test_include_filters(hass):
|
|||
hass.states.async_set(
|
||||
'group.allow', 'off', {'friendly_name': "Allowed group"})
|
||||
|
||||
config = smart_home.Config(filter=entityfilter.generate_filter(
|
||||
config = smart_home.Config(should_expose=entityfilter.generate_filter(
|
||||
include_domains=['automation', 'group'],
|
||||
include_entities=['script.deny'],
|
||||
exclude_domains=[],
|
||||
|
@ -1116,3 +1116,40 @@ def test_api_mute(hass, domain):
|
|||
assert len(call) == 1
|
||||
assert call[0].data['entity_id'] == '{}.test'.format(domain)
|
||||
assert msg['header']['name'] == 'Response'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_entity_config(hass):
|
||||
"""Test that we can configure things via entity config."""
|
||||
request = get_new_request('Alexa.Discovery', 'Discover')
|
||||
|
||||
hass.states.async_set(
|
||||
'light.test_1', 'on', {'friendly_name': "Test light 1"})
|
||||
|
||||
config = smart_home.Config(
|
||||
should_expose=lambda entity_id: True,
|
||||
entity_config={
|
||||
'light.test_1': {
|
||||
'name': 'Config name',
|
||||
'display_categories': 'SWITCH',
|
||||
'description': 'Config description'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
msg = yield from smart_home.async_handle_message(
|
||||
hass, config, request)
|
||||
|
||||
assert 'event' in msg
|
||||
msg = msg['event']
|
||||
|
||||
assert len(msg['payload']['endpoints']) == 1
|
||||
|
||||
appliance = msg['payload']['endpoints'][0]
|
||||
assert appliance['endpointId'] == 'light#test_1'
|
||||
assert appliance['displayCategories'][0] == "SWITCH"
|
||||
assert appliance['friendlyName'] == "Config name"
|
||||
assert appliance['description'] == "Config description"
|
||||
assert len(appliance['capabilities']) == 1
|
||||
assert appliance['capabilities'][-1]['interface'] == \
|
||||
'Alexa.PowerController'
|
||||
|
|
|
@ -38,16 +38,6 @@ def mock_cloud():
|
|||
return MagicMock(subscription_expired=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cloud_instance(loop, hass):
|
||||
"""Instance of an initialized cloud class."""
|
||||
with patch('homeassistant.components.cloud.Cloud.initialize',
|
||||
return_value=mock_coro(True)):
|
||||
loop.run_until_complete(async_setup_component(hass, 'cloud', {}))
|
||||
|
||||
yield hass.data['cloud']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_cloud_calling_handler(mock_client, mock_handle_message, mock_cloud):
|
||||
"""Test we call handle message with correct info."""
|
||||
|
@ -269,13 +259,35 @@ def test_refresh_token_before_expiration_fails(hass, mock_cloud):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_handler_alexa(hass, cloud_instance):
|
||||
def test_handler_alexa(hass):
|
||||
"""Test handler Alexa."""
|
||||
hass.states.async_set(
|
||||
'switch.test', 'on', {'friendly_name': "Test switch"})
|
||||
hass.states.async_set(
|
||||
'switch.test2', 'on', {'friendly_name': "Test switch 2"})
|
||||
|
||||
with patch('homeassistant.components.cloud.Cloud.initialize',
|
||||
return_value=mock_coro(True)):
|
||||
setup = yield from async_setup_component(hass, 'cloud', {
|
||||
'cloud': {
|
||||
'alexa': {
|
||||
'filter': {
|
||||
'exclude_entities': 'switch.test2'
|
||||
},
|
||||
'entity_config': {
|
||||
'switch.test': {
|
||||
'name': 'Config name',
|
||||
'description': 'Config description',
|
||||
'display_categories': 'LIGHT'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
assert setup
|
||||
|
||||
resp = yield from iot.async_handle_alexa(
|
||||
hass, cloud_instance,
|
||||
hass, hass.data['cloud'],
|
||||
test_alexa.get_new_request('Alexa.Discovery', 'Discover'))
|
||||
|
||||
endpoints = resp['event']['payload']['endpoints']
|
||||
|
@ -283,16 +295,32 @@ def test_handler_alexa(hass, cloud_instance):
|
|||
assert len(endpoints) == 1
|
||||
device = endpoints[0]
|
||||
|
||||
assert device['description'] == 'switch.test'
|
||||
assert device['friendlyName'] == 'Test switch'
|
||||
assert device['description'] == 'Config description'
|
||||
assert device['friendlyName'] == 'Config name'
|
||||
assert device['displayCategories'] == ['LIGHT']
|
||||
assert device['manufacturerName'] == 'Home Assistant'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_handler_google_actions(hass, cloud_instance):
|
||||
def test_handler_google_actions(hass):
|
||||
"""Test handler Google Actions."""
|
||||
hass.states.async_set(
|
||||
'switch.test', 'on', {'friendly_name': "Test switch"})
|
||||
hass.states.async_set(
|
||||
'switch.test2', 'on', {'friendly_name': "Test switch 2"})
|
||||
|
||||
with patch('homeassistant.components.cloud.Cloud.initialize',
|
||||
return_value=mock_coro(True)):
|
||||
setup = yield from async_setup_component(hass, 'cloud', {
|
||||
'cloud': {
|
||||
'google_actions': {
|
||||
'filter': {
|
||||
'exclude_entities': 'switch.test2'
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
assert setup
|
||||
|
||||
reqid = '5711642932632160983'
|
||||
data = {'requestId': reqid, 'inputs': [{'intent': 'action.devices.SYNC'}]}
|
||||
|
@ -300,7 +328,7 @@ def test_handler_google_actions(hass, cloud_instance):
|
|||
with patch('homeassistant.components.cloud.Cloud._decode_claims',
|
||||
return_value={'cognito:username': 'myUserName'}):
|
||||
resp = yield from iot.async_handle_google_actions(
|
||||
hass, cloud_instance, data)
|
||||
hass, hass.data['cloud'], data)
|
||||
|
||||
assert resp['requestId'] == reqid
|
||||
payload = resp['payload']
|
||||
|
|
Loading…
Add table
Reference in a new issue