Implement entity and domain exclude/include for Alexa (#10647)

* Implement entity and domain exclude/include for Alexa

* Switch to using generate_filter

* Use proper domain for turn on/off calls except for groups where we must use the generic homeassistant.turn_on/off

* travis fixes

* Untangle

* Lint
This commit is contained in:
Robbie Trencheny 2017-11-17 21:10:24 -08:00 committed by Paulus Schoutsen
parent 64a393b377
commit 1317297191
7 changed files with 233 additions and 75 deletions

View File

@ -1,5 +1,6 @@
"""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
@ -72,8 +73,11 @@ MAPPING_COMPONENT = {
} }
Config = namedtuple('AlexaConfig', 'filter')
@asyncio.coroutine @asyncio.coroutine
def async_handle_message(hass, message): def async_handle_message(hass, config, message):
"""Handle incoming API messages.""" """Handle incoming API messages."""
assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3' assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
@ -89,7 +93,7 @@ def async_handle_message(hass, message):
"Unsupported API request %s/%s", namespace, name) "Unsupported API request %s/%s", namespace, name)
return api_error(message) return api_error(message)
return (yield from funct_ref(hass, message)) return (yield from funct_ref(hass, config, message))
def api_message(request, name='Response', namespace='Alexa', payload=None): def api_message(request, name='Response', namespace='Alexa', payload=None):
@ -138,7 +142,7 @@ def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
@HANDLERS.register(('Alexa.Discovery', 'Discover')) @HANDLERS.register(('Alexa.Discovery', 'Discover'))
@asyncio.coroutine @asyncio.coroutine
def async_api_discovery(hass, request): def async_api_discovery(hass, config, request):
"""Create a API formatted discovery response. """Create a API formatted discovery response.
Async friendly. Async friendly.
@ -146,7 +150,14 @@ def async_api_discovery(hass, 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):
_LOGGER.debug("Not exposing %s because filtered by config",
entity.entity_id)
continue
if entity.attributes.get(ATTR_ALEXA_HIDDEN, False): if entity.attributes.get(ATTR_ALEXA_HIDDEN, False):
_LOGGER.debug("Not exposing %s because alexa_hidden is true",
entity.entity_id)
continue continue
class_data = MAPPING_COMPONENT.get(entity.domain) class_data = MAPPING_COMPONENT.get(entity.domain)
@ -207,7 +218,7 @@ def async_api_discovery(hass, request):
def extract_entity(funct): def extract_entity(funct):
"""Decorator for extract entity object from request.""" """Decorator for extract entity object from request."""
@asyncio.coroutine @asyncio.coroutine
def async_api_entity_wrapper(hass, request): def async_api_entity_wrapper(hass, config, request):
"""Process a turn on request.""" """Process a turn on request."""
entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.') entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')
@ -218,7 +229,7 @@ def extract_entity(funct):
request[API_HEADER]['name'], entity_id) request[API_HEADER]['name'], entity_id)
return api_error(request, error_type='NO_SUCH_ENDPOINT') return api_error(request, error_type='NO_SUCH_ENDPOINT')
return (yield from funct(hass, request, entity)) return (yield from funct(hass, config, request, entity))
return async_api_entity_wrapper return async_api_entity_wrapper
@ -226,9 +237,13 @@ def extract_entity(funct):
@HANDLERS.register(('Alexa.PowerController', 'TurnOn')) @HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_turn_on(hass, request, entity): def async_api_turn_on(hass, config, request, entity):
"""Process a turn on request.""" """Process a turn on request."""
yield from hass.services.async_call(ha.DOMAIN, SERVICE_TURN_ON, { domain = entity.domain
if entity.domain == group.DOMAIN:
domain = ha.DOMAIN
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
}, blocking=True) }, blocking=True)
@ -238,9 +253,13 @@ def async_api_turn_on(hass, request, entity):
@HANDLERS.register(('Alexa.PowerController', 'TurnOff')) @HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_turn_off(hass, request, entity): def async_api_turn_off(hass, config, request, entity):
"""Process a turn off request.""" """Process a turn off request."""
yield from hass.services.async_call(ha.DOMAIN, SERVICE_TURN_OFF, { domain = entity.domain
if entity.domain == group.DOMAIN:
domain = ha.DOMAIN
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
}, blocking=True) }, blocking=True)
@ -250,7 +269,7 @@ def async_api_turn_off(hass, request, entity):
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness')) @HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_set_brightness(hass, request, entity): def async_api_set_brightness(hass, config, request, entity):
"""Process a set brightness request.""" """Process a set brightness request."""
brightness = int(request[API_PAYLOAD]['brightness']) brightness = int(request[API_PAYLOAD]['brightness'])
@ -265,7 +284,7 @@ def async_api_set_brightness(hass, request, entity):
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness')) @HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_adjust_brightness(hass, request, entity): def async_api_adjust_brightness(hass, config, request, entity):
"""Process a adjust brightness request.""" """Process a adjust brightness request."""
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta']) brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
@ -289,7 +308,7 @@ def async_api_adjust_brightness(hass, request, entity):
@HANDLERS.register(('Alexa.ColorController', 'SetColor')) @HANDLERS.register(('Alexa.ColorController', 'SetColor'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_set_color(hass, request, entity): def async_api_set_color(hass, config, request, entity):
"""Process a set color request.""" """Process a set color request."""
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES) supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
rgb = color_util.color_hsb_to_RGB( rgb = color_util.color_hsb_to_RGB(
@ -317,7 +336,7 @@ def async_api_set_color(hass, request, entity):
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature')) @HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_set_color_temperature(hass, request, entity): def async_api_set_color_temperature(hass, config, request, entity):
"""Process a set color temperature request.""" """Process a set color temperature request."""
kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin']) kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin'])
@ -333,7 +352,7 @@ def async_api_set_color_temperature(hass, request, entity):
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature')) ('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_decrease_color_temp(hass, request, entity): def async_api_decrease_color_temp(hass, config, request, entity):
"""Process a decrease color temperature request.""" """Process a decrease color temperature request."""
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS)) max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
@ -351,7 +370,7 @@ def async_api_decrease_color_temp(hass, request, entity):
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature')) ('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_increase_color_temp(hass, request, entity): def async_api_increase_color_temp(hass, config, request, entity):
"""Process a increase color temperature request.""" """Process a increase color temperature request."""
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS)) min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
@ -368,7 +387,7 @@ def async_api_increase_color_temp(hass, request, entity):
@HANDLERS.register(('Alexa.SceneController', 'Activate')) @HANDLERS.register(('Alexa.SceneController', 'Activate'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_activate(hass, request, entity): def async_api_activate(hass, config, request, entity):
"""Process a activate request.""" """Process a activate request."""
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
@ -380,7 +399,7 @@ def async_api_activate(hass, request, entity):
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage')) @HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_set_percentage(hass, request, entity): def async_api_set_percentage(hass, config, request, entity):
"""Process a set percentage request.""" """Process a set percentage request."""
percentage = int(request[API_PAYLOAD]['percentage']) percentage = int(request[API_PAYLOAD]['percentage'])
service = None service = None
@ -411,7 +430,7 @@ def async_api_set_percentage(hass, request, entity):
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage')) @HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_adjust_percentage(hass, request, entity): def async_api_adjust_percentage(hass, config, request, entity):
"""Process a adjust percentage request.""" """Process a adjust percentage request."""
percentage_delta = int(request[API_PAYLOAD]['percentageDelta']) percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
service = None service = None
@ -459,7 +478,7 @@ def async_api_adjust_percentage(hass, request, entity):
@HANDLERS.register(('Alexa.LockController', 'Lock')) @HANDLERS.register(('Alexa.LockController', 'Lock'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_lock(hass, request, entity): def async_api_lock(hass, config, request, entity):
"""Process a lock request.""" """Process a lock request."""
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, { yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
@ -472,7 +491,7 @@ def async_api_lock(hass, request, entity):
@HANDLERS.register(('Alexa.LockController', 'Unlock')) @HANDLERS.register(('Alexa.LockController', 'Unlock'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_unlock(hass, request, entity): def async_api_unlock(hass, config, request, entity):
"""Process a unlock request.""" """Process a unlock request."""
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, { yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
@ -484,7 +503,7 @@ def async_api_unlock(hass, request, entity):
@HANDLERS.register(('Alexa.Speaker', 'SetVolume')) @HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_set_volume(hass, request, entity): def async_api_set_volume(hass, config, request, entity):
"""Process a set volume request.""" """Process a set volume request."""
volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2) volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
@ -502,7 +521,7 @@ def async_api_set_volume(hass, request, entity):
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume')) @HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_adjust_volume(hass, request, entity): def async_api_adjust_volume(hass, config, request, entity):
"""Process a adjust volume request.""" """Process a adjust volume request."""
volume_delta = int(request[API_PAYLOAD]['volume']) volume_delta = int(request[API_PAYLOAD]['volume'])
@ -531,7 +550,7 @@ def async_api_adjust_volume(hass, request, entity):
@HANDLERS.register(('Alexa.Speaker', 'SetMute')) @HANDLERS.register(('Alexa.Speaker', 'SetMute'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_set_mute(hass, request, entity): def async_api_set_mute(hass, config, request, entity):
"""Process a set mute request.""" """Process a set mute request."""
mute = bool(request[API_PAYLOAD]['mute']) mute = bool(request[API_PAYLOAD]['mute'])
@ -550,7 +569,7 @@ def async_api_set_mute(hass, request, entity):
@HANDLERS.register(('Alexa.PlaybackController', 'Play')) @HANDLERS.register(('Alexa.PlaybackController', 'Play'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_play(hass, request, entity): def async_api_play(hass, config, request, entity):
"""Process a play request.""" """Process a play request."""
data = { data = {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
@ -565,7 +584,7 @@ def async_api_play(hass, request, entity):
@HANDLERS.register(('Alexa.PlaybackController', 'Pause')) @HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_pause(hass, request, entity): def async_api_pause(hass, config, request, entity):
"""Process a pause request.""" """Process a pause request."""
data = { data = {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
@ -580,7 +599,7 @@ def async_api_pause(hass, request, entity):
@HANDLERS.register(('Alexa.PlaybackController', 'Stop')) @HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_stop(hass, request, entity): def async_api_stop(hass, config, request, entity):
"""Process a stop request.""" """Process a stop request."""
data = { data = {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
@ -595,7 +614,7 @@ def async_api_stop(hass, request, entity):
@HANDLERS.register(('Alexa.PlaybackController', 'Next')) @HANDLERS.register(('Alexa.PlaybackController', 'Next'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_next(hass, request, entity): def async_api_next(hass, config, request, entity):
"""Process a next request.""" """Process a next request."""
data = { data = {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
@ -611,7 +630,7 @@ def async_api_next(hass, request, entity):
@HANDLERS.register(('Alexa.PlaybackController', 'Previous')) @HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_previous(hass, request, entity): def async_api_previous(hass, config, request, entity):
"""Process a previous request.""" """Process a previous request."""
data = { data = {
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id

View File

@ -9,7 +9,9 @@ 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.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.components.alexa import smart_home
from . import http_api, iot from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS from .const import CONFIG_DIR, DOMAIN, SERVERS
@ -18,6 +20,8 @@ REQUIREMENTS = ['warrant==0.5.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ALEXA = 'alexa'
CONF_ALEXA_FILTER = 'filter'
CONF_COGNITO_CLIENT_ID = 'cognito_client_id' CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
CONF_RELAYER = 'relayer' CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id' CONF_USER_POOL_ID = 'user_pool_id'
@ -26,6 +30,13 @@ MODE_DEV = 'development'
DEFAULT_MODE = MODE_DEV DEFAULT_MODE = MODE_DEV
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
ALEXA_SCHEMA = vol.Schema({
vol.Optional(
CONF_ALEXA_FILTER,
default=lambda: entityfilter.generate_filter([], [], [], [])
): entityfilter.FILTER_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):
@ -35,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USER_POOL_ID): str, vol.Required(CONF_USER_POOL_ID): str,
vol.Required(CONF_REGION): str, vol.Required(CONF_REGION): str,
vol.Required(CONF_RELAYER): str, vol.Required(CONF_RELAYER): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -47,6 +59,10 @@ def async_setup(hass, config):
else: else:
kwargs = {CONF_MODE: DEFAULT_MODE} kwargs = {CONF_MODE: DEFAULT_MODE}
if CONF_ALEXA not in kwargs:
kwargs[CONF_ALEXA] = ALEXA_SCHEMA({})
kwargs[CONF_ALEXA] = smart_home.Config(**kwargs[CONF_ALEXA])
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
@asyncio.coroutine @asyncio.coroutine
@ -64,10 +80,11 @@ class Cloud:
"""Store the configuration of the cloud connection.""" """Store the configuration of the cloud connection."""
def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None, def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None,
region=None, relayer=None): region=None, relayer=None, alexa=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.id_token = None self.id_token = None
self.access_token = None self.access_token = None
self.refresh_token = None self.refresh_token = None

View File

@ -206,7 +206,9 @@ def async_handle_message(hass, cloud, handler_name, payload):
@asyncio.coroutine @asyncio.coroutine
def async_handle_alexa(hass, cloud, payload): def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa.""" """Handle an incoming IoT message for Alexa."""
return (yield from smart_home.async_handle_message(hass, payload)) return (yield from smart_home.async_handle_message(hass,
cloud.alexa_config,
payload))
@HANDLERS.register('cloud') @HANDLERS.register('cloud')

View File

@ -25,7 +25,17 @@ DEPENDENCIES = ['mqtt']
DOMAIN = 'mqtt_statestream' DOMAIN = 'mqtt_statestream'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: cv.FILTER_SCHEMA.extend({ DOMAIN: vol.Schema({
vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string])
}),
vol.Optional(CONF_INCLUDE, default={}): vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string])
}),
vol.Required(CONF_BASE_TOPIC): valid_publish_topic, vol.Required(CONF_BASE_TOPIC): valid_publish_topic,
vol.Optional(CONF_PUBLISH_ATTRIBUTES, default=False): cv.boolean, vol.Optional(CONF_PUBLISH_ATTRIBUTES, default=False): cv.boolean,
vol.Optional(CONF_PUBLISH_TIMESTAMPS, default=False): cv.boolean vol.Optional(CONF_PUBLISH_TIMESTAMPS, default=False): cv.boolean

View File

@ -12,8 +12,7 @@ import voluptuous as vol
from homeassistant.loader import get_platform from homeassistant.loader import get_platform
from homeassistant.const import ( from homeassistant.const import (
CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, CONF_PLATFORM, CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS, CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS,
CONF_CONDITION, CONF_BELOW, CONF_ABOVE, CONF_TIMEOUT, SUN_EVENT_SUNSET, CONF_CONDITION, CONF_BELOW, CONF_ABOVE, CONF_TIMEOUT, SUN_EVENT_SUNSET,
SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC) SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC)
@ -563,16 +562,3 @@ SCRIPT_SCHEMA = vol.All(
[vol.Any(SERVICE_SCHEMA, _SCRIPT_DELAY_SCHEMA, [vol.Any(SERVICE_SCHEMA, _SCRIPT_DELAY_SCHEMA,
_SCRIPT_WAIT_TEMPLATE_SCHEMA, EVENT_SCHEMA, CONDITION_SCHEMA)], _SCRIPT_WAIT_TEMPLATE_SCHEMA, EVENT_SCHEMA, CONDITION_SCHEMA)],
) )
FILTER_SCHEMA = vol.Schema({
vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): entity_ids,
vol.Optional(CONF_DOMAINS, default=[]):
vol.All(ensure_list, [string])
}),
vol.Optional(CONF_INCLUDE, default={}): vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): entity_ids,
vol.Optional(CONF_DOMAINS, default=[]):
vol.All(ensure_list, [string])
})
})

View File

@ -1,6 +1,30 @@
"""Helper class to implement include/exclude of entities and domains.""" """Helper class to implement include/exclude of entities and domains."""
import voluptuous as vol
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
from homeassistant.helpers import config_validation as cv
CONF_INCLUDE_DOMAINS = 'include_domains'
CONF_INCLUDE_ENTITIES = 'include_entities'
CONF_EXCLUDE_DOMAINS = 'exclude_domains'
CONF_EXCLUDE_ENTITIES = 'exclude_entities'
FILTER_SCHEMA = vol.All(
vol.Schema({
vol.Optional(CONF_EXCLUDE_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EXCLUDE_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_INCLUDE_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_INCLUDE_ENTITIES, default=[]): cv.entity_ids,
}),
lambda config: generate_filter(
config[CONF_INCLUDE_DOMAINS],
config[CONF_INCLUDE_ENTITIES],
config[CONF_EXCLUDE_DOMAINS],
config[CONF_EXCLUDE_ENTITIES],
))
def generate_filter(include_domains, include_entities, def generate_filter(include_domains, include_entities,

View File

@ -5,9 +5,12 @@ from uuid import uuid4
import pytest import pytest
from homeassistant.components.alexa import smart_home from homeassistant.components.alexa import smart_home
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)
def get_new_request(namespace, name, endpoint=None): def get_new_request(namespace, name, endpoint=None):
"""Generate a new API message.""" """Generate a new API message."""
@ -91,7 +94,7 @@ def test_wrong_version(hass):
msg['directive']['header']['payloadVersion'] = '2' msg['directive']['header']['payloadVersion'] = '2'
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
yield from smart_home.async_handle_message(hass, msg) yield from smart_home.async_handle_message(hass, DEFAULT_CONFIG, msg)
@asyncio.coroutine @asyncio.coroutine
@ -157,7 +160,8 @@ def test_discovery_request(hass):
'position': 85 'position': 85
}) })
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -319,6 +323,67 @@ def test_discovery_request(hass):
raise AssertionError("Unknown appliance!") raise AssertionError("Unknown appliance!")
@asyncio.coroutine
def test_exclude_filters(hass):
"""Test exclusion filters."""
request = get_new_request('Alexa.Discovery', 'Discover')
# setup test devices
hass.states.async_set(
'switch.test', 'on', {'friendly_name': "Test switch"})
hass.states.async_set(
'script.deny', 'off', {'friendly_name': "Blocked script"})
hass.states.async_set(
'cover.deny', 'off', {'friendly_name': "Blocked cover"})
config = smart_home.Config(filter=entityfilter.generate_filter(
include_domains=[],
include_entities=[],
exclude_domains=['script'],
exclude_entities=['cover.deny'],
))
msg = yield from smart_home.async_handle_message(hass, config, request)
msg = msg['event']
assert len(msg['payload']['endpoints']) == 1
@asyncio.coroutine
def test_include_filters(hass):
"""Test inclusion filters."""
request = get_new_request('Alexa.Discovery', 'Discover')
# setup test devices
hass.states.async_set(
'switch.deny', 'on', {'friendly_name': "Blocked switch"})
hass.states.async_set(
'script.deny', 'off', {'friendly_name': "Blocked script"})
hass.states.async_set(
'automation.allow', 'off', {'friendly_name': "Allowed automation"})
hass.states.async_set(
'group.allow', 'off', {'friendly_name': "Allowed group"})
config = smart_home.Config(filter=entityfilter.generate_filter(
include_domains=['automation', 'group'],
include_entities=['script.deny'],
exclude_domains=[],
exclude_entities=[],
))
msg = yield from smart_home.async_handle_message(hass, config, request)
msg = msg['event']
assert len(msg['payload']['endpoints']) == 3
@asyncio.coroutine @asyncio.coroutine
def test_api_entity_not_exists(hass): def test_api_entity_not_exists(hass):
"""Test api turn on process without entity.""" """Test api turn on process without entity."""
@ -326,7 +391,8 @@ def test_api_entity_not_exists(hass):
call_switch = async_mock_service(hass, 'switch', 'turn_on') call_switch = async_mock_service(hass, 'switch', 'turn_on')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -341,7 +407,8 @@ def test_api_entity_not_exists(hass):
def test_api_function_not_implemented(hass): def test_api_function_not_implemented(hass):
"""Test api call that is not implemented to us.""" """Test api call that is not implemented to us."""
request = get_new_request('Alexa.HAHAAH', 'Sweet') request = get_new_request('Alexa.HAHAAH', 'Sweet')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -366,9 +433,15 @@ def test_api_turn_on(hass, domain):
'friendly_name': "Test {}".format(domain) 'friendly_name': "Test {}".format(domain)
}) })
call = async_mock_service(hass, 'homeassistant', 'turn_on') call_domain = domain
msg = yield from smart_home.async_handle_message(hass, request) if domain == 'group':
call_domain = 'homeassistant'
call = async_mock_service(hass, call_domain, 'turn_on')
msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -393,9 +466,15 @@ def test_api_turn_off(hass, domain):
'friendly_name': "Test {}".format(domain) 'friendly_name': "Test {}".format(domain)
}) })
call = async_mock_service(hass, 'homeassistant', 'turn_off') call_domain = domain
msg = yield from smart_home.async_handle_message(hass, request) if domain == 'group':
call_domain = 'homeassistant'
call = async_mock_service(hass, call_domain, 'turn_off')
msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -420,7 +499,8 @@ def test_api_set_brightness(hass):
call_light = async_mock_service(hass, 'light', 'turn_on') call_light = async_mock_service(hass, 'light', 'turn_on')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -450,7 +530,8 @@ def test_api_adjust_brightness(hass, result, adjust):
call_light = async_mock_service(hass, 'light', 'turn_on') call_light = async_mock_service(hass, 'light', 'turn_on')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -483,7 +564,8 @@ def test_api_set_color_rgb(hass):
call_light = async_mock_service(hass, 'light', 'turn_on') call_light = async_mock_service(hass, 'light', 'turn_on')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -516,7 +598,8 @@ def test_api_set_color_xy(hass):
call_light = async_mock_service(hass, 'light', 'turn_on') call_light = async_mock_service(hass, 'light', 'turn_on')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -544,7 +627,8 @@ def test_api_set_color_temperature(hass):
call_light = async_mock_service(hass, 'light', 'turn_on') call_light = async_mock_service(hass, 'light', 'turn_on')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -572,7 +656,8 @@ def test_api_decrease_color_temp(hass, result, initial):
call_light = async_mock_service(hass, 'light', 'turn_on') call_light = async_mock_service(hass, 'light', 'turn_on')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -600,7 +685,8 @@ def test_api_increase_color_temp(hass, result, initial):
call_light = async_mock_service(hass, 'light', 'turn_on') call_light = async_mock_service(hass, 'light', 'turn_on')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -626,7 +712,8 @@ def test_api_activate(hass, domain):
call = async_mock_service(hass, domain, 'turn_on') call = async_mock_service(hass, domain, 'turn_on')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -651,7 +738,8 @@ def test_api_set_percentage_fan(hass):
call_fan = async_mock_service(hass, 'fan', 'set_speed') call_fan = async_mock_service(hass, 'fan', 'set_speed')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -679,7 +767,8 @@ def test_api_set_percentage_cover(hass):
call_cover = async_mock_service(hass, 'cover', 'set_cover_position') call_cover = async_mock_service(hass, 'cover', 'set_cover_position')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -709,7 +798,8 @@ def test_api_adjust_percentage_fan(hass, result, adjust):
call_fan = async_mock_service(hass, 'fan', 'set_speed') call_fan = async_mock_service(hass, 'fan', 'set_speed')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -740,7 +830,8 @@ def test_api_adjust_percentage_cover(hass, result, adjust):
call_cover = async_mock_service(hass, 'cover', 'set_cover_position') call_cover = async_mock_service(hass, 'cover', 'set_cover_position')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -766,7 +857,8 @@ def test_api_lock(hass, domain):
call = async_mock_service(hass, domain, 'lock') call = async_mock_service(hass, domain, 'lock')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -791,7 +883,8 @@ def test_api_play(hass, domain):
call = async_mock_service(hass, domain, 'media_play') call = async_mock_service(hass, domain, 'media_play')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -816,7 +909,8 @@ def test_api_pause(hass, domain):
call = async_mock_service(hass, domain, 'media_pause') call = async_mock_service(hass, domain, 'media_pause')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -841,7 +935,8 @@ def test_api_stop(hass, domain):
call = async_mock_service(hass, domain, 'media_stop') call = async_mock_service(hass, domain, 'media_stop')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -866,7 +961,8 @@ def test_api_next(hass, domain):
call = async_mock_service(hass, domain, 'media_next_track') call = async_mock_service(hass, domain, 'media_next_track')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -891,7 +987,8 @@ def test_api_previous(hass, domain):
call = async_mock_service(hass, domain, 'media_previous_track') call = async_mock_service(hass, domain, 'media_previous_track')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -918,7 +1015,8 @@ def test_api_set_volume(hass):
call_media_player = async_mock_service(hass, 'media_player', 'volume_set') call_media_player = async_mock_service(hass, 'media_player', 'volume_set')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -948,7 +1046,8 @@ def test_api_adjust_volume(hass, result, adjust):
call_media_player = async_mock_service(hass, 'media_player', 'volume_set') call_media_player = async_mock_service(hass, 'media_player', 'volume_set')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
@ -976,7 +1075,8 @@ def test_api_mute(hass, domain):
call = async_mock_service(hass, domain, 'volume_mute') call = async_mock_service(hass, domain, 'volume_mute')
msg = yield from smart_home.async_handle_message(hass, request) msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']