diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index a5ed4b59628..b683f5cfc7c 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -10,17 +10,34 @@ import logging import voluptuous as vol from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import entityfilter -from . import flash_briefings, intent +from . import flash_briefings, intent, smart_home from .const import ( - CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN) + CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, + CONF_FILTER, CONF_ENTITY_CONFIG) _LOGGER = logging.getLogger(__name__) CONF_FLASH_BRIEFINGS = 'flash_briefings' +CONF_SMART_HOME = 'smart_home' DEPENDENCIES = ['http'] +ALEXA_ENTITY_SCHEMA = vol.Schema({ + vol.Optional(smart_home.CONF_DESCRIPTION): cv.string, + vol.Optional(smart_home.CONF_DISPLAY_CATEGORIES): cv.string, + vol.Optional(smart_home.CONF_NAME): cv.string, +}) + +SMART_HOME_SCHEMA = vol.Schema({ + vol.Optional( + CONF_FILTER, + default=lambda: entityfilter.generate_filter([], [], [], []) + ): entityfilter.FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA} +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: { CONF_FLASH_BRIEFINGS: { @@ -31,7 +48,10 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_TEXT, default=""): cv.template, vol.Optional(CONF_DISPLAY_URL): cv.template, }]), - } + }, + # vol.Optional here would mean we couldn't distinguish between an empty + # smart_home: and none at all. + CONF_SMART_HOME: vol.Any(SMART_HOME_SCHEMA, None), } }, extra=vol.ALLOW_EXTRA) @@ -47,4 +67,12 @@ def async_setup(hass, config): if flash_briefings_config: flash_briefings.async_setup(hass, flash_briefings_config) + try: + smart_home_config = config[CONF_SMART_HOME] + except KeyError: + pass + else: + smart_home_config = smart_home_config or SMART_HOME_SCHEMA({}) + smart_home.async_setup(hass, smart_home_config) + return True diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index c243fc12d5e..7d6489b535a 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -8,6 +8,9 @@ CONF_AUDIO = 'audio' CONF_TEXT = 'text' CONF_DISPLAY_URL = 'display_url' +CONF_FILTER = 'filter' +CONF_ENTITY_CONFIG = 'entity_config' + ATTR_UID = 'uid' ATTR_UPDATE_DATE = 'updateDate' ATTR_TITLE_TEXT = 'titleText' diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 7360b22a89f..70b6f3cd9bc 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -7,16 +7,17 @@ from uuid import uuid4 from homeassistant.components import ( alert, automation, cover, fan, group, input_boolean, light, lock, - media_player, scene, script, switch) + media_player, scene, script, switch, http) +import homeassistant.core as ha +import homeassistant.util.color as color_util +from homeassistant.util.decorator import Registry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_SET) -import homeassistant.core as ha -import homeassistant.util.color as color_util -from homeassistant.util.decorator import Registry +from .const import CONF_FILTER, CONF_ENTITY_CONFIG _LOGGER = logging.getLogger(__name__) @@ -26,6 +27,8 @@ API_EVENT = 'event' API_HEADER = 'header' API_PAYLOAD = 'payload' +SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home' + CONF_DESCRIPTION = 'description' CONF_DISPLAY_CATEGORIES = 'display_categories' @@ -113,6 +116,51 @@ class Config: self.entity_config = entity_config or {} +@ha.callback +def async_setup(hass, config): + """Activate Smart Home functionality of Alexa component. + + This is optional, triggered by having a `smart_home:` sub-section in the + alexa configuration. + + 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. + """ + smart_home_config = Config( + should_expose=config[CONF_FILTER], + entity_config=config.get(CONF_ENTITY_CONFIG), + ) + hass.http.register_view(SmartHomeView(smart_home_config)) + + +class SmartHomeView(http.HomeAssistantView): + """Expose Smart Home v3 payload interface via HTTP POST.""" + + url = SMART_HOME_HTTP_ENDPOINT + name = 'api:alexa:smart_home' + + def __init__(self, smart_home_config): + """Initialize.""" + self.smart_home_config = smart_home_config + + @asyncio.coroutine + def post(self, request): + """Handle Alexa Smart Home requests. + + The Smart Home API requires the endpoint to be implemented in AWS + Lambda, which will need to forward the requests to here and pass back + the response. + """ + hass = request.app['hass'] + message = yield from request.json() + + _LOGGER.debug("Received Alexa Smart Home request: %s", message) + + response = yield from async_handle_message( + hass, self.smart_home_config, message) + return b'' if response is None else self.json(response) + + @asyncio.coroutine def async_handle_message(hass, config, message): """Handle incoming API messages.""" diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 1d98b87b960..330f2c254bb 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,9 +1,12 @@ """Test for smart home alexa support.""" import asyncio +import json from uuid import uuid4 import pytest +from homeassistant.setup import async_setup_component +from homeassistant.components import alexa from homeassistant.components.alexa import smart_home from homeassistant.helpers import entityfilter @@ -1155,3 +1158,45 @@ def test_entity_config(hass): assert len(appliance['capabilities']) == 1 assert appliance['capabilities'][-1]['interface'] == \ 'Alexa.PowerController' + + +@asyncio.coroutine +def do_http_discovery(config, hass, test_client): + """Submit a request to the Smart Home HTTP API.""" + yield from async_setup_component(hass, alexa.DOMAIN, config) + http_client = yield from test_client(hass.http.app) + + request = get_new_request('Alexa.Discovery', 'Discover') + response = yield from http_client.post( + smart_home.SMART_HOME_HTTP_ENDPOINT, + data=json.dumps(request), + headers={'content-type': 'application/json'}) + return response + + +@asyncio.coroutine +def test_http_api(hass, test_client): + """With `smart_home:` HTTP API is exposed.""" + config = { + 'alexa': { + 'smart_home': None + } + } + + response = yield from do_http_discovery(config, hass, test_client) + response_data = yield from response.json() + + # Here we're testing just the HTTP view glue -- details of discovery are + # covered in other tests. + assert response_data['event']['header']['name'] == 'Discover.Response' + + +@asyncio.coroutine +def test_http_api_disabled(hass, test_client): + """Without `smart_home:`, the HTTP API is disabled.""" + config = { + 'alexa': {} + } + response = yield from do_http_discovery(config, hass, test_client) + + assert response.status == 404