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:
Paulus Schoutsen 2018-01-05 12:33:22 -08:00 committed by GitHub
parent 71fb7a6ef6
commit 8b57777ce9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 140 additions and 57 deletions

View File

@ -1,6 +1,5 @@
"""Support for alexa Smart Home Skill API.""" """Support for alexa Smart Home Skill API."""
import asyncio import asyncio
from collections import namedtuple
import logging import logging
import math import math
from uuid import uuid4 from uuid import uuid4
@ -27,10 +26,9 @@ API_EVENT = 'event'
API_HEADER = 'header' API_HEADER = 'header'
API_PAYLOAD = 'payload' API_PAYLOAD = 'payload'
ATTR_ALEXA_DESCRIPTION = 'alexa_description' CONF_DESCRIPTION = 'description'
ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories' CONF_DISPLAY_CATEGORIES = 'display_categories'
ATTR_ALEXA_HIDDEN = 'alexa_hidden' CONF_NAME = 'name'
ATTR_ALEXA_NAME = 'alexa_name'
MAPPING_COMPONENT = { 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 @asyncio.coroutine
@ -150,32 +154,28 @@ def async_api_discovery(hass, config, request):
discovery_endpoints = [] discovery_endpoints = []
for entity in hass.states.async_all(): 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", _LOGGER.debug("Not exposing %s because filtered by config",
entity.entity_id) entity.entity_id)
continue 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) class_data = MAPPING_COMPONENT.get(entity.domain)
if not class_data: if not class_data:
continue continue
friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name) entity_conf = config.entity_config.get(entity.entity_id, {})
description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION,
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 # Required description as per Amazon Scene docs
if entity.domain == scene.DOMAIN: if entity.domain == scene.DOMAIN:
scene_fmt = '{} (Scene connected via Home Assistant)' scene_fmt = '{} (Scene connected via Home Assistant)'
description = scene_fmt.format(description) description = scene_fmt.format(description)
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES display_categories = entity_conf.get(CONF_DISPLAY_CATEGORIES,
display_categories = entity.attributes.get(cat_key, class_data[0]) class_data[0])
endpoint = { endpoint = {
'displayCategories': [display_categories], 'displayCategories': [display_categories],

View File

@ -12,6 +12,7 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE) EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE)
from homeassistant.helpers import entityfilter from homeassistant.helpers import entityfilter
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.alexa import smart_home as alexa_sh
@ -25,7 +26,7 @@ REQUIREMENTS = ['warrant==0.6.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ALEXA = 'alexa' CONF_ALEXA = 'alexa'
CONF_GOOGLE_ASSISTANT = 'google_assistant' CONF_GOOGLE_ACTIONS = 'google_actions'
CONF_FILTER = 'filter' CONF_FILTER = 'filter'
CONF_COGNITO_CLIENT_ID = 'cognito_client_id' CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
CONF_RELAYER = 'relayer' CONF_RELAYER = 'relayer'
@ -35,6 +36,14 @@ MODE_DEV = 'development'
DEFAULT_MODE = 'production' DEFAULT_MODE = 'production'
DEPENDENCIES = ['http'] 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({ ASSISTANT_SCHEMA = vol.Schema({
vol.Optional( vol.Optional(
CONF_FILTER, CONF_FILTER,
@ -42,6 +51,10 @@ ASSISTANT_SCHEMA = vol.Schema({
): entityfilter.FILTER_SCHEMA, ): entityfilter.FILTER_SCHEMA,
}) })
ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
})
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE): 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_USER_POOL_ID): str,
vol.Optional(CONF_REGION): str, vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str, vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_ALEXA): ASSISTANT_SCHEMA, vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ASSISTANT): ASSISTANT_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): ASSISTANT_SCHEMA,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -61,18 +74,19 @@ CONFIG_SCHEMA = vol.Schema({
def async_setup(hass, config): def async_setup(hass, config):
"""Initialize the Home Assistant cloud.""" """Initialize the Home Assistant cloud."""
if DOMAIN in config: if DOMAIN in config:
kwargs = config[DOMAIN] kwargs = dict(config[DOMAIN])
else: else:
kwargs = {CONF_MODE: DEFAULT_MODE} kwargs = {CONF_MODE: DEFAULT_MODE}
if CONF_ALEXA not in kwargs: alexa_conf = kwargs.pop(CONF_ALEXA, None) or ALEXA_SCHEMA({})
kwargs[CONF_ALEXA] = ASSISTANT_SCHEMA({}) gactions_conf = (kwargs.pop(CONF_GOOGLE_ACTIONS, None) or
ASSISTANT_SCHEMA({}))
if CONF_GOOGLE_ASSISTANT not in kwargs: kwargs[CONF_ALEXA] = alexa_sh.Config(
kwargs[CONF_GOOGLE_ASSISTANT] = ASSISTANT_SCHEMA({}) should_expose=alexa_conf[CONF_FILTER],
entity_config=alexa_conf.get(CONF_ENTITY_CONFIG),
kwargs[CONF_ALEXA] = alexa_sh.Config(**kwargs[CONF_ALEXA]) )
kwargs['gass_should_expose'] = kwargs.pop(CONF_GOOGLE_ASSISTANT)['filter'] kwargs['gactions_should_expose'] = gactions_conf[CONF_FILTER]
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
success = yield from cloud.initialize() success = yield from cloud.initialize()
@ -87,15 +101,15 @@ def async_setup(hass, config):
class Cloud: class Cloud:
"""Store the configuration of the cloud connection.""" """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, cognito_client_id=None, user_pool_id=None, region=None,
relayer=None): relayer=None):
"""Create an instance of Cloud.""" """Create an instance of Cloud."""
self.hass = hass self.hass = hass
self.mode = mode self.mode = mode
self.alexa_config = alexa self.alexa_config = alexa
self._gass_should_expose = gass_should_expose self._gactions_should_expose = gactions_should_expose
self._gass_config = None self._gactions_config = None
self.jwt_keyset = None self.jwt_keyset = None
self.id_token = None self.id_token = None
self.access_token = None self.access_token = None
@ -144,15 +158,19 @@ class Cloud:
return self.path('{}_auth.json'.format(self.mode)) return self.path('{}_auth.json'.format(self.mode))
@property @property
def gass_config(self): def gactions_config(self):
"""Return the Google Assistant config.""" """Return the Google Assistant config."""
if self._gass_config is None: if self._gactions_config is None:
self._gass_config = ga_sh.Config( def should_expose(entity):
should_expose=self._gass_should_expose, """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'] agent_user_id=self.claims['cognito:username']
) )
return self._gass_config return self._gactions_config
@asyncio.coroutine @asyncio.coroutine
def initialize(self): def initialize(self):
@ -182,7 +200,7 @@ class Cloud:
self.id_token = None self.id_token = None
self.access_token = None self.access_token = None
self.refresh_token = None self.refresh_token = None
self._gass_config = None self._gactions_config = None
yield from self.hass.async_add_job( yield from self.hass.async_add_job(
lambda: os.remove(self.user_info_path)) lambda: os.remove(self.user_info_path))

View File

@ -214,7 +214,7 @@ def async_handle_alexa(hass, cloud, payload):
@asyncio.coroutine @asyncio.coroutine
def async_handle_google_actions(hass, cloud, payload): def async_handle_google_actions(hass, cloud, payload):
"""Handle an incoming IoT message for Google Actions.""" """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) payload)
return result return result

View File

@ -9,7 +9,7 @@ from homeassistant.helpers import entityfilter
from tests.common import async_mock_service 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): def get_new_request(namespace, name, endpoint=None):
@ -338,7 +338,7 @@ 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"})
config = smart_home.Config(filter=entityfilter.generate_filter( config = smart_home.Config(should_expose=entityfilter.generate_filter(
include_domains=[], include_domains=[],
include_entities=[], include_entities=[],
exclude_domains=['script'], exclude_domains=['script'],
@ -371,7 +371,7 @@ 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"})
config = smart_home.Config(filter=entityfilter.generate_filter( config = smart_home.Config(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=[],
@ -1116,3 +1116,40 @@ def test_api_mute(hass, domain):
assert len(call) == 1 assert len(call) == 1
assert call[0].data['entity_id'] == '{}.test'.format(domain) assert call[0].data['entity_id'] == '{}.test'.format(domain)
assert msg['header']['name'] == 'Response' 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'

View File

@ -38,16 +38,6 @@ def mock_cloud():
return MagicMock(subscription_expired=False) 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 @asyncio.coroutine
def test_cloud_calling_handler(mock_client, mock_handle_message, mock_cloud): def test_cloud_calling_handler(mock_client, mock_handle_message, mock_cloud):
"""Test we call handle message with correct info.""" """Test we call handle message with correct info."""
@ -269,13 +259,35 @@ def test_refresh_token_before_expiration_fails(hass, mock_cloud):
@asyncio.coroutine @asyncio.coroutine
def test_handler_alexa(hass, cloud_instance): def test_handler_alexa(hass):
"""Test handler Alexa.""" """Test handler Alexa."""
hass.states.async_set( hass.states.async_set(
'switch.test', 'on', {'friendly_name': "Test switch"}) '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( resp = yield from iot.async_handle_alexa(
hass, cloud_instance, hass, hass.data['cloud'],
test_alexa.get_new_request('Alexa.Discovery', 'Discover')) test_alexa.get_new_request('Alexa.Discovery', 'Discover'))
endpoints = resp['event']['payload']['endpoints'] endpoints = resp['event']['payload']['endpoints']
@ -283,16 +295,32 @@ def test_handler_alexa(hass, cloud_instance):
assert len(endpoints) == 1 assert len(endpoints) == 1
device = endpoints[0] device = endpoints[0]
assert device['description'] == 'switch.test' assert device['description'] == 'Config description'
assert device['friendlyName'] == 'Test switch' assert device['friendlyName'] == 'Config name'
assert device['displayCategories'] == ['LIGHT']
assert device['manufacturerName'] == 'Home Assistant' assert device['manufacturerName'] == 'Home Assistant'
@asyncio.coroutine @asyncio.coroutine
def test_handler_google_actions(hass, cloud_instance): def test_handler_google_actions(hass):
"""Test handler Google Actions.""" """Test handler Google Actions."""
hass.states.async_set( hass.states.async_set(
'switch.test', 'on', {'friendly_name': "Test switch"}) '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' reqid = '5711642932632160983'
data = {'requestId': reqid, 'inputs': [{'intent': 'action.devices.SYNC'}]} 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', with patch('homeassistant.components.cloud.Cloud._decode_claims',
return_value={'cognito:username': 'myUserName'}): return_value={'cognito:username': 'myUserName'}):
resp = yield from iot.async_handle_google_actions( resp = yield from iot.async_handle_google_actions(
hass, cloud_instance, data) hass, hass.data['cloud'], data)
assert resp['requestId'] == reqid assert resp['requestId'] == reqid
payload = resp['payload'] payload = resp['payload']