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."""
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],

View file

@ -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))

View file

@ -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

View file

@ -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'

View file

@ -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']