Expose Alexa Smart Home via HTTP POST (#11859)
* Expose Alexa Smart Home via HTTP POST Haaska uses the deprecated v2 Alexa Smart Home payload. Exposing the v3 implementation this way allows an easy path to upgrading Haaska and reducing code duplication with Home Assistant Cloud. * Expose Alexa Smart Home via HTTP POST Haaska uses the deprecated v2 Alexa Smart Home payload. Exposing the v3 implementation this way allows an easy path to upgrading Haaska and reducing code duplication with Home Assistant Cloud.
This commit is contained in:
parent
73fa76d792
commit
990e076c2c
4 changed files with 131 additions and 7 deletions
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue