2017-09-16 19:35:28 +00:00
|
|
|
"""Support for alexa Smart Home Skill API."""
|
|
|
|
import asyncio
|
|
|
|
import logging
|
2017-11-01 03:28:17 +00:00
|
|
|
import math
|
2017-09-16 19:35:28 +00:00
|
|
|
from uuid import uuid4
|
|
|
|
|
|
|
|
from homeassistant.const import (
|
|
|
|
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
|
|
|
from homeassistant.components import switch, light
|
2017-11-01 03:28:17 +00:00
|
|
|
import homeassistant.util.color as color_util
|
2017-09-28 19:26:27 +00:00
|
|
|
from homeassistant.util.decorator import Registry
|
2017-09-16 19:35:28 +00:00
|
|
|
|
2017-09-28 19:26:27 +00:00
|
|
|
HANDLERS = Registry()
|
2017-09-16 19:35:28 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
API_DIRECTIVE = 'directive'
|
|
|
|
API_EVENT = 'event'
|
|
|
|
API_HEADER = 'header'
|
|
|
|
API_PAYLOAD = 'payload'
|
|
|
|
API_ENDPOINT = 'endpoint'
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
MAPPING_COMPONENT = {
|
2017-10-07 20:31:57 +00:00
|
|
|
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
|
2017-09-16 19:35:28 +00:00
|
|
|
light.DOMAIN: [
|
2017-10-07 20:31:57 +00:00
|
|
|
'LIGHT', ('Alexa.PowerController',), {
|
2017-11-01 03:28:17 +00:00
|
|
|
light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController',
|
|
|
|
light.SUPPORT_RGB_COLOR: 'Alexa.ColorController',
|
|
|
|
light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController',
|
2017-09-16 19:35:28 +00:00
|
|
|
}
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_handle_message(hass, message):
|
2017-09-23 15:15:46 +00:00
|
|
|
"""Handle incoming API messages."""
|
2017-10-07 20:31:57 +00:00
|
|
|
assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
|
|
|
|
|
|
|
|
# Read head data
|
|
|
|
message = message[API_DIRECTIVE]
|
|
|
|
namespace = message[API_HEADER]['namespace']
|
|
|
|
name = message[API_HEADER]['name']
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
# Do we support this API request?
|
2017-10-07 20:31:57 +00:00
|
|
|
funct_ref = HANDLERS.get((namespace, name))
|
2017-09-16 19:35:28 +00:00
|
|
|
if not funct_ref:
|
|
|
|
_LOGGER.warning(
|
2017-10-07 20:31:57 +00:00
|
|
|
"Unsupported API request %s/%s", namespace, name)
|
2017-09-16 19:35:28 +00:00
|
|
|
return api_error(message)
|
|
|
|
|
|
|
|
return (yield from funct_ref(hass, message))
|
|
|
|
|
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
def api_message(request, name='Response', namespace='Alexa', payload=None):
|
2017-09-23 15:15:46 +00:00
|
|
|
"""Create a API formatted response message.
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
|
|
|
payload = payload or {}
|
2017-10-07 20:31:57 +00:00
|
|
|
|
|
|
|
response = {
|
|
|
|
API_EVENT: {
|
|
|
|
API_HEADER: {
|
|
|
|
'namespace': namespace,
|
|
|
|
'name': name,
|
|
|
|
'messageId': str(uuid4()),
|
|
|
|
'payloadVersion': '3',
|
|
|
|
},
|
|
|
|
API_PAYLOAD: payload,
|
|
|
|
}
|
2017-09-16 19:35:28 +00:00
|
|
|
}
|
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
# If a correlation token exsits, add it to header / Need by Async requests
|
|
|
|
token = request[API_HEADER].get('correlationToken')
|
|
|
|
if token:
|
|
|
|
response[API_EVENT][API_HEADER]['correlationToken'] = token
|
|
|
|
|
|
|
|
# Extend event with endpoint object / Need by Async requests
|
|
|
|
if API_ENDPOINT in request:
|
|
|
|
response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy()
|
2017-09-16 19:35:28 +00:00
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
|
2017-09-23 15:15:46 +00:00
|
|
|
"""Create a API formatted error response.
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2017-10-07 20:31:57 +00:00
|
|
|
payload = {
|
|
|
|
'type': error_type,
|
|
|
|
'message': error_message,
|
|
|
|
}
|
2017-09-16 19:35:28 +00:00
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
return api_message(request, name='ErrorResponse', payload=payload)
|
2017-09-16 19:35:28 +00:00
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
2017-09-16 19:35:28 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_discovery(hass, request):
|
2017-09-23 15:15:46 +00:00
|
|
|
"""Create a API formatted discovery response.
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2017-10-07 20:31:57 +00:00
|
|
|
discovery_endpoints = []
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
for entity in hass.states.async_all():
|
|
|
|
class_data = MAPPING_COMPONENT.get(entity.domain)
|
|
|
|
|
|
|
|
if not class_data:
|
|
|
|
continue
|
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
endpoint = {
|
|
|
|
'displayCategories': [class_data[0]],
|
2017-09-16 19:35:28 +00:00
|
|
|
'additionalApplianceDetails': {},
|
2017-10-07 20:31:57 +00:00
|
|
|
'endpointId': entity.entity_id.replace('.', '#'),
|
2017-09-16 19:35:28 +00:00
|
|
|
'friendlyName': entity.name,
|
2017-10-07 20:31:57 +00:00
|
|
|
'description': '',
|
2017-09-16 19:35:28 +00:00
|
|
|
'manufacturerName': 'Unknown',
|
|
|
|
}
|
2017-10-07 20:31:57 +00:00
|
|
|
actions = set()
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
# static actions
|
|
|
|
if class_data[1]:
|
2017-10-07 20:31:57 +00:00
|
|
|
actions |= set(class_data[1])
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
# dynamic actions
|
|
|
|
if class_data[2]:
|
|
|
|
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
for feature, action_name in class_data[2].items():
|
|
|
|
if feature & supported > 0:
|
2017-10-07 20:31:57 +00:00
|
|
|
actions.add(action_name)
|
|
|
|
|
|
|
|
# Write action into capabilities
|
|
|
|
capabilities = []
|
|
|
|
for action in actions:
|
|
|
|
capabilities.append({
|
|
|
|
'type': 'AlexaInterface',
|
|
|
|
'interface': action,
|
|
|
|
'version': 3,
|
|
|
|
})
|
2017-09-16 19:35:28 +00:00
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
endpoint['capabilities'] = capabilities
|
|
|
|
discovery_endpoints.append(endpoint)
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
return api_message(
|
2017-10-07 20:31:57 +00:00
|
|
|
request, name='Discover.Response', namespace='Alexa.Discovery',
|
|
|
|
payload={'endpoints': discovery_endpoints})
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
def extract_entity(funct):
|
|
|
|
"""Decorator for extract entity object from request."""
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_entity_wrapper(hass, request):
|
|
|
|
"""Process a turn on request."""
|
2017-10-07 20:31:57 +00:00
|
|
|
entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
# extract state object
|
|
|
|
entity = hass.states.get(entity_id)
|
|
|
|
if not entity:
|
|
|
|
_LOGGER.error("Can't process %s for %s",
|
2017-10-07 20:31:57 +00:00
|
|
|
request[API_HEADER]['name'], entity_id)
|
|
|
|
return api_error(request, error_type='NO_SUCH_ENDPOINT')
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
return (yield from funct(hass, request, entity))
|
|
|
|
|
|
|
|
return async_api_entity_wrapper
|
|
|
|
|
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
|
2017-09-16 19:35:28 +00:00
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_turn_on(hass, request, entity):
|
|
|
|
"""Process a turn on request."""
|
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
|
|
|
}, blocking=True)
|
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
return api_message(request)
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
|
2017-09-16 19:35:28 +00:00
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_turn_off(hass, request, entity):
|
|
|
|
"""Process a turn off request."""
|
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_TURN_OFF, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
|
|
|
}, blocking=True)
|
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
return api_message(request)
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
|
2017-09-16 19:35:28 +00:00
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-10-07 20:31:57 +00:00
|
|
|
def async_api_set_brightness(hass, request, entity):
|
|
|
|
"""Process a set brightness request."""
|
2017-11-01 03:28:17 +00:00
|
|
|
brightness = int(request[API_PAYLOAD]['brightness'])
|
2017-09-16 19:35:28 +00:00
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
2017-11-01 03:28:17 +00:00
|
|
|
light.ATTR_BRIGHTNESS_PCT: brightness,
|
|
|
|
}, blocking=True)
|
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_adjust_brightness(hass, request, entity):
|
|
|
|
"""Process a adjust brightness request."""
|
|
|
|
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
|
|
|
|
|
|
|
|
# read current state
|
|
|
|
try:
|
|
|
|
current = math.floor(
|
|
|
|
int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100)
|
|
|
|
except ZeroDivisionError:
|
|
|
|
return api_error(request, error_type='INVALID_VALUE')
|
|
|
|
|
|
|
|
# set brightness
|
|
|
|
brightness = brightness_delta + current
|
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
light.ATTR_BRIGHTNESS_PCT: brightness,
|
|
|
|
}, blocking=True)
|
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_set_color(hass, request, entity):
|
|
|
|
"""Process a set color request."""
|
|
|
|
hue = float(request[API_PAYLOAD]['color']['hue'])
|
|
|
|
saturation = float(request[API_PAYLOAD]['color']['saturation'])
|
|
|
|
brightness = float(request[API_PAYLOAD]['color']['brightness'])
|
|
|
|
|
|
|
|
rgb = color_util.color_hsb_to_RGB(hue, saturation, brightness)
|
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
light.ATTR_RGB_COLOR: rgb,
|
|
|
|
}, blocking=True)
|
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_set_color_temperature(hass, request, entity):
|
|
|
|
"""Process a set color temperature request."""
|
|
|
|
kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin'])
|
|
|
|
|
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
light.ATTR_KELVIN: kelvin,
|
|
|
|
}, blocking=True)
|
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(
|
|
|
|
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_decrease_color_temp(hass, request, entity):
|
|
|
|
"""Process a decrease color temperature request."""
|
|
|
|
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
|
|
|
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
|
|
|
|
|
|
|
|
value = min(max_mireds, current + 50)
|
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
light.ATTR_COLOR_TEMP: value,
|
|
|
|
}, blocking=True)
|
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(
|
|
|
|
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_increase_color_temp(hass, request, entity):
|
|
|
|
"""Process a increase color temperature request."""
|
|
|
|
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
|
|
|
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
|
|
|
|
|
|
|
|
value = max(min_mireds, current - 50)
|
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
light.ATTR_COLOR_TEMP: value,
|
2017-10-07 20:31:57 +00:00
|
|
|
}, blocking=True)
|
|
|
|
|
|
|
|
return api_message(request)
|