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
|
2018-01-23 08:01:18 +00:00
|
|
|
from datetime import datetime
|
2017-09-16 19:35:28 +00:00
|
|
|
from uuid import uuid4
|
|
|
|
|
2018-01-21 06:35:38 +00:00
|
|
|
from homeassistant.components import (
|
|
|
|
alert, automation, cover, fan, group, input_boolean, light, lock,
|
2018-01-26 18:40:39 +00:00
|
|
|
media_player, scene, script, switch, http, sensor)
|
2018-01-23 18:45:28 +00:00
|
|
|
import homeassistant.core as ha
|
|
|
|
import homeassistant.util.color as color_util
|
|
|
|
from homeassistant.util.decorator import Registry
|
2017-09-16 19:35:28 +00:00
|
|
|
from homeassistant.const import (
|
2018-01-21 06:35:38 +00:00
|
|
|
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK,
|
2017-11-17 17:14:22 +00:00
|
|
|
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,
|
2018-01-26 18:40:39 +00:00
|
|
|
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
2018-01-29 01:00:34 +00:00
|
|
|
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
|
2018-01-23 18:45:28 +00:00
|
|
|
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
API_DIRECTIVE = 'directive'
|
2017-11-17 17:14:22 +00:00
|
|
|
API_ENDPOINT = 'endpoint'
|
2017-10-07 20:31:57 +00:00
|
|
|
API_EVENT = 'event'
|
2018-01-26 18:40:39 +00:00
|
|
|
API_CONTEXT = 'context'
|
2017-10-07 20:31:57 +00:00
|
|
|
API_HEADER = 'header'
|
|
|
|
API_PAYLOAD = 'payload'
|
2017-11-17 17:14:22 +00:00
|
|
|
|
2018-01-26 18:40:39 +00:00
|
|
|
API_TEMP_UNITS = {
|
|
|
|
TEMP_FAHRENHEIT: 'FAHRENHEIT',
|
|
|
|
TEMP_CELSIUS: 'CELSIUS',
|
|
|
|
}
|
|
|
|
|
2018-01-23 18:45:28 +00:00
|
|
|
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
|
|
|
|
2018-01-05 20:33:22 +00:00
|
|
|
CONF_DESCRIPTION = 'description'
|
|
|
|
CONF_DISPLAY_CATEGORIES = 'display_categories'
|
2017-09-16 19:35:28 +00:00
|
|
|
|
2018-01-21 06:35:38 +00:00
|
|
|
HANDLERS = Registry()
|
2018-01-29 00:43:27 +00:00
|
|
|
ENTITY_ADAPTERS = Registry()
|
2017-09-16 19:35:28 +00:00
|
|
|
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
class _DisplayCategory(object):
|
|
|
|
"""Possible display categories for Discovery response.
|
|
|
|
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Describes a combination of devices set to a specific state, when the
|
|
|
|
# state change must occur in a specific order. For example, a "watch
|
2018-01-29 22:37:19 +00:00
|
|
|
# Netflix" scene might require the: 1. TV to be powered on & 2. Input set
|
|
|
|
# to HDMI1. Applies to Scenes
|
2018-01-26 05:06:57 +00:00
|
|
|
ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
|
|
|
|
|
|
|
|
# Indicates media devices with video or photo capabilities.
|
|
|
|
CAMERA = "CAMERA"
|
|
|
|
|
|
|
|
# Indicates a door.
|
|
|
|
DOOR = "DOOR"
|
|
|
|
|
|
|
|
# Indicates light sources or fixtures.
|
|
|
|
LIGHT = "LIGHT"
|
|
|
|
|
|
|
|
# An endpoint that cannot be described in on of the other categories.
|
|
|
|
OTHER = "OTHER"
|
|
|
|
|
|
|
|
# Describes a combination of devices set to a specific state, when the
|
|
|
|
# order of the state change is not important. For example a bedtime scene
|
|
|
|
# might include turning off lights and lowering the thermostat, but the
|
|
|
|
# order is unimportant. Applies to Scenes
|
|
|
|
SCENE_TRIGGER = "SCENE_TRIGGER"
|
|
|
|
|
|
|
|
# Indicates an endpoint that locks.
|
|
|
|
SMARTLOCK = "SMARTLOCK"
|
|
|
|
|
|
|
|
# Indicates modules that are plugged into an existing electrical outlet.
|
|
|
|
# Can control a variety of devices.
|
|
|
|
SMARTPLUG = "SMARTPLUG"
|
|
|
|
|
|
|
|
# Indicates the endpoint is a speaker or speaker system.
|
|
|
|
SPEAKER = "SPEAKER"
|
|
|
|
|
|
|
|
# Indicates in-wall switches wired to the electrical system. Can control a
|
|
|
|
# variety of devices.
|
|
|
|
SWITCH = "SWITCH"
|
|
|
|
|
|
|
|
# Indicates endpoints that report the temperature only.
|
|
|
|
TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR"
|
|
|
|
|
|
|
|
# Indicates endpoints that control temperature, stand-alone air
|
|
|
|
# conditioners, or heaters with direct temperature control.
|
|
|
|
THERMOSTAT = "THERMOSTAT"
|
|
|
|
|
|
|
|
# Indicates the endpoint is a television.
|
|
|
|
# pylint: disable=invalid-name
|
|
|
|
TV = "TV"
|
|
|
|
|
|
|
|
|
|
|
|
def _capability(interface,
|
|
|
|
version=3,
|
|
|
|
supports_deactivation=None,
|
2018-01-26 18:40:39 +00:00
|
|
|
retrievable=None,
|
|
|
|
properties_supported=None,
|
2018-01-26 05:06:57 +00:00
|
|
|
cap_type='AlexaInterface'):
|
|
|
|
"""Return a Smart Home API capability object.
|
|
|
|
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#capability-object
|
|
|
|
|
|
|
|
There are some additional fields allowed but not implemented here since
|
|
|
|
we've no use case for them yet:
|
|
|
|
|
|
|
|
- proactively_reported
|
|
|
|
|
|
|
|
`supports_deactivation` applies only to scenes.
|
|
|
|
"""
|
|
|
|
result = {
|
|
|
|
'type': cap_type,
|
|
|
|
'interface': interface,
|
|
|
|
'version': version,
|
|
|
|
}
|
|
|
|
|
|
|
|
if supports_deactivation is not None:
|
|
|
|
result['supportsDeactivation'] = supports_deactivation
|
|
|
|
|
2018-01-26 18:40:39 +00:00
|
|
|
if retrievable is not None:
|
|
|
|
result['retrievable'] = retrievable
|
|
|
|
|
|
|
|
if properties_supported is not None:
|
|
|
|
result['properties'] = {'supported': properties_supported}
|
|
|
|
|
2018-01-26 05:06:57 +00:00
|
|
|
return result
|
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
class _UnsupportedInterface(Exception):
|
|
|
|
"""This entity does not support the requested Smart Home API interface."""
|
|
|
|
|
|
|
|
|
|
|
|
class _UnsupportedProperty(Exception):
|
|
|
|
"""This entity does not support the requested Smart Home API property."""
|
|
|
|
|
|
|
|
|
|
|
|
class _AlexaEntity(object):
|
|
|
|
"""An adaptation of an entity, expressed in Alexa's terms.
|
|
|
|
|
|
|
|
The API handlers should manipulate entities only through this interface.
|
|
|
|
"""
|
|
|
|
|
2018-01-26 05:06:57 +00:00
|
|
|
def __init__(self, config, entity):
|
|
|
|
self.config = config
|
|
|
|
self.entity = entity
|
2018-01-29 00:43:27 +00:00
|
|
|
self.entity_conf = config.entity_config.get(entity.entity_id, {})
|
|
|
|
|
|
|
|
def friendly_name(self):
|
|
|
|
"""Return the Alexa API friendly name."""
|
|
|
|
return self.entity_conf.get(CONF_NAME, self.entity.name)
|
|
|
|
|
|
|
|
def description(self):
|
|
|
|
"""Return the Alexa API description."""
|
|
|
|
return self.entity_conf.get(CONF_DESCRIPTION, self.entity.entity_id)
|
|
|
|
|
|
|
|
def entity_id(self):
|
|
|
|
"""Return the Alexa API entity id."""
|
|
|
|
return self.entity.entity_id.replace('.', '#')
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
def display_categories(self):
|
|
|
|
"""Return a list of display categories."""
|
|
|
|
entity_conf = self.config.entity_config.get(self.entity.entity_id, {})
|
|
|
|
if CONF_DISPLAY_CATEGORIES in entity_conf:
|
|
|
|
return [entity_conf[CONF_DISPLAY_CATEGORIES]]
|
|
|
|
return self.default_display_categories()
|
|
|
|
|
|
|
|
def default_display_categories(self):
|
|
|
|
"""Return a list of default display categories.
|
|
|
|
|
|
|
|
This can be overridden by the user in the Home Assistant configuration.
|
|
|
|
|
|
|
|
See also _DisplayCategory.
|
|
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def get_interface(self, capability):
|
|
|
|
"""Return the given _AlexaInterface.
|
2018-01-26 05:06:57 +00:00
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
Raises _UnsupportedInterface.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
def interfaces(self):
|
|
|
|
"""Return a list of supported interfaces.
|
2018-01-26 18:40:39 +00:00
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
Used for discovery. The list should contain _AlexaInterface instances.
|
|
|
|
If the list is empty, this entity will not be discovered.
|
2018-01-26 05:06:57 +00:00
|
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
class _AlexaInterface(object):
|
|
|
|
def __init__(self, entity):
|
|
|
|
self.entity = entity
|
|
|
|
|
|
|
|
def name(self):
|
|
|
|
"""Return the Alexa API name of this interface."""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def properties_supported():
|
|
|
|
"""Return what properties this entity supports."""
|
|
|
|
return []
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def properties_proactively_reported():
|
|
|
|
"""Return True if properties asynchronously reported."""
|
|
|
|
return False
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def properties_retrievable():
|
|
|
|
"""Return True if properties can be retrieved."""
|
|
|
|
return False
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_property(name):
|
|
|
|
"""Read and return a property.
|
|
|
|
|
|
|
|
Return value should be a dict, or raise _UnsupportedProperty.
|
|
|
|
|
|
|
|
Properties can also have a timeOfSample and uncertaintyInMilliseconds,
|
|
|
|
but returning those metadata is not yet implemented.
|
|
|
|
"""
|
|
|
|
raise _UnsupportedProperty(name)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def supports_deactivation():
|
|
|
|
"""Applicable only to scenes."""
|
|
|
|
return None
|
|
|
|
|
|
|
|
def serialize_discovery(self):
|
|
|
|
"""Serialize according to the Discovery API."""
|
|
|
|
result = {
|
|
|
|
'type': 'AlexaInterface',
|
|
|
|
'interface': self.name(),
|
|
|
|
'version': '3',
|
|
|
|
'properties': {
|
|
|
|
'supported': self.properties_supported(),
|
|
|
|
'proactivelyReported': self.properties_proactively_reported(),
|
2018-01-29 01:00:34 +00:00
|
|
|
'retrievable': self.properties_retrievable(),
|
2018-01-29 00:43:27 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
# pylint: disable=assignment-from-none
|
|
|
|
supports_deactivation = self.supports_deactivation()
|
|
|
|
if supports_deactivation is not None:
|
|
|
|
result['supportsDeactivation'] = supports_deactivation
|
|
|
|
return result
|
|
|
|
|
|
|
|
def serialize_properties(self):
|
|
|
|
"""Return properties serialized for an API response."""
|
|
|
|
for prop in self.properties_supported():
|
|
|
|
prop_name = prop['name']
|
|
|
|
yield {
|
|
|
|
'name': prop_name,
|
|
|
|
'namespace': self.name(),
|
|
|
|
'value': self.get_property(prop_name),
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _AlexaPowerController(_AlexaInterface):
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.PowerController'
|
|
|
|
|
2018-01-29 01:00:34 +00:00
|
|
|
def properties_supported(self):
|
|
|
|
return [{'name': 'powerState'}]
|
|
|
|
|
|
|
|
def properties_retrievable(self):
|
|
|
|
return True
|
|
|
|
|
|
|
|
def get_property(self, name):
|
|
|
|
if name != 'powerState':
|
|
|
|
raise _UnsupportedProperty(name)
|
|
|
|
|
|
|
|
if self.entity.state == STATE_ON:
|
|
|
|
return 'ON'
|
|
|
|
return 'OFF'
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
|
|
|
|
class _AlexaLockController(_AlexaInterface):
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.LockController'
|
|
|
|
|
2018-01-29 01:00:34 +00:00
|
|
|
def properties_supported(self):
|
|
|
|
return [{'name': 'lockState'}]
|
|
|
|
|
|
|
|
def properties_retrievable(self):
|
|
|
|
return True
|
|
|
|
|
|
|
|
def get_property(self, name):
|
|
|
|
if name != 'lockState':
|
|
|
|
raise _UnsupportedProperty(name)
|
|
|
|
|
|
|
|
if self.entity.state == STATE_LOCKED:
|
|
|
|
return 'LOCKED'
|
|
|
|
elif self.entity.state == STATE_UNLOCKED:
|
|
|
|
return 'UNLOCKED'
|
|
|
|
return 'JAMMED'
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
|
|
|
|
class _AlexaSceneController(_AlexaInterface):
|
|
|
|
def __init__(self, entity, supports_deactivation):
|
|
|
|
_AlexaInterface.__init__(self, entity)
|
|
|
|
self.supports_deactivation = lambda: supports_deactivation
|
|
|
|
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.SceneController'
|
|
|
|
|
|
|
|
|
|
|
|
class _AlexaBrightnessController(_AlexaInterface):
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.BrightnessController'
|
|
|
|
|
2018-01-29 01:00:34 +00:00
|
|
|
def properties_supported(self):
|
|
|
|
return [{'name': 'brightness'}]
|
|
|
|
|
|
|
|
def properties_retrievable(self):
|
|
|
|
return True
|
|
|
|
|
|
|
|
def get_property(self, name):
|
|
|
|
if name != 'brightness':
|
|
|
|
raise _UnsupportedProperty(name)
|
2018-02-12 06:36:22 +00:00
|
|
|
if 'brightness' in self.entity.attributes:
|
|
|
|
return round(self.entity.attributes['brightness'] / 255.0 * 100)
|
|
|
|
return 0
|
2018-01-29 01:00:34 +00:00
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
|
|
|
|
class _AlexaColorController(_AlexaInterface):
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.ColorController'
|
|
|
|
|
|
|
|
|
|
|
|
class _AlexaColorTemperatureController(_AlexaInterface):
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.ColorTemperatureController'
|
|
|
|
|
|
|
|
|
|
|
|
class _AlexaPercentageController(_AlexaInterface):
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.PercentageController'
|
|
|
|
|
|
|
|
|
|
|
|
class _AlexaSpeaker(_AlexaInterface):
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.Speaker'
|
|
|
|
|
|
|
|
|
2018-02-06 00:02:08 +00:00
|
|
|
class _AlexaStepSpeaker(_AlexaInterface):
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.StepSpeaker'
|
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
class _AlexaPlaybackController(_AlexaInterface):
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.PlaybackController'
|
|
|
|
|
|
|
|
|
2018-01-29 06:22:04 +00:00
|
|
|
class _AlexaInputController(_AlexaInterface):
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.InputController'
|
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
class _AlexaTemperatureSensor(_AlexaInterface):
|
|
|
|
def name(self):
|
|
|
|
return 'Alexa.TemperatureSensor'
|
|
|
|
|
|
|
|
def properties_supported(self):
|
|
|
|
return [{'name': 'temperature'}]
|
|
|
|
|
|
|
|
def properties_retrievable(self):
|
|
|
|
return True
|
|
|
|
|
|
|
|
def get_property(self, name):
|
|
|
|
if name != 'temperature':
|
|
|
|
raise _UnsupportedProperty(name)
|
|
|
|
|
|
|
|
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
|
|
|
return {
|
|
|
|
'value': float(self.entity.state),
|
|
|
|
'scale': API_TEMP_UNITS[unit],
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ENTITY_ADAPTERS.register(alert.DOMAIN)
|
|
|
|
@ENTITY_ADAPTERS.register(automation.DOMAIN)
|
|
|
|
@ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
|
|
|
|
class _GenericCapabilities(_AlexaEntity):
|
2018-01-26 05:06:57 +00:00
|
|
|
"""A generic, on/off device.
|
|
|
|
|
|
|
|
The choice of last resort.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def default_display_categories(self):
|
|
|
|
return [_DisplayCategory.OTHER]
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def interfaces(self):
|
|
|
|
return [_AlexaPowerController(self.entity)]
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
@ENTITY_ADAPTERS.register(switch.DOMAIN)
|
|
|
|
class _SwitchCapabilities(_AlexaEntity):
|
2018-01-26 05:06:57 +00:00
|
|
|
def default_display_categories(self):
|
|
|
|
return [_DisplayCategory.SWITCH]
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def interfaces(self):
|
|
|
|
return [_AlexaPowerController(self.entity)]
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
|
|
|
class _CoverCapabilities(_AlexaEntity):
|
2018-01-26 05:06:57 +00:00
|
|
|
def default_display_categories(self):
|
|
|
|
return [_DisplayCategory.DOOR]
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def interfaces(self):
|
|
|
|
yield _AlexaPowerController(self.entity)
|
2018-01-26 05:06:57 +00:00
|
|
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
if supported & cover.SUPPORT_SET_POSITION:
|
2018-01-29 00:43:27 +00:00
|
|
|
yield _AlexaPercentageController(self.entity)
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
@ENTITY_ADAPTERS.register(light.DOMAIN)
|
|
|
|
class _LightCapabilities(_AlexaEntity):
|
2018-01-26 05:06:57 +00:00
|
|
|
def default_display_categories(self):
|
|
|
|
return [_DisplayCategory.LIGHT]
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def interfaces(self):
|
|
|
|
yield _AlexaPowerController(self.entity)
|
|
|
|
|
2018-01-26 05:06:57 +00:00
|
|
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
if supported & light.SUPPORT_BRIGHTNESS:
|
2018-01-29 00:43:27 +00:00
|
|
|
yield _AlexaBrightnessController(self.entity)
|
2018-01-26 05:06:57 +00:00
|
|
|
if supported & light.SUPPORT_RGB_COLOR:
|
2018-01-29 00:43:27 +00:00
|
|
|
yield _AlexaColorController(self.entity)
|
2018-01-26 05:06:57 +00:00
|
|
|
if supported & light.SUPPORT_XY_COLOR:
|
2018-01-29 00:43:27 +00:00
|
|
|
yield _AlexaColorController(self.entity)
|
2018-01-26 05:06:57 +00:00
|
|
|
if supported & light.SUPPORT_COLOR_TEMP:
|
2018-01-29 00:43:27 +00:00
|
|
|
yield _AlexaColorTemperatureController(self.entity)
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
@ENTITY_ADAPTERS.register(fan.DOMAIN)
|
|
|
|
class _FanCapabilities(_AlexaEntity):
|
2018-01-26 05:06:57 +00:00
|
|
|
def default_display_categories(self):
|
|
|
|
return [_DisplayCategory.OTHER]
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def interfaces(self):
|
|
|
|
yield _AlexaPowerController(self.entity)
|
2018-01-26 05:06:57 +00:00
|
|
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
if supported & fan.SUPPORT_SET_SPEED:
|
2018-01-29 00:43:27 +00:00
|
|
|
yield _AlexaPercentageController(self.entity)
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
@ENTITY_ADAPTERS.register(lock.DOMAIN)
|
|
|
|
class _LockCapabilities(_AlexaEntity):
|
2018-01-26 05:06:57 +00:00
|
|
|
def default_display_categories(self):
|
|
|
|
return [_DisplayCategory.SMARTLOCK]
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def interfaces(self):
|
|
|
|
return [_AlexaLockController(self.entity)]
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
@ENTITY_ADAPTERS.register(media_player.DOMAIN)
|
|
|
|
class _MediaPlayerCapabilities(_AlexaEntity):
|
2018-01-26 05:06:57 +00:00
|
|
|
def default_display_categories(self):
|
|
|
|
return [_DisplayCategory.TV]
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def interfaces(self):
|
|
|
|
yield _AlexaPowerController(self.entity)
|
|
|
|
|
2018-01-26 05:06:57 +00:00
|
|
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
if supported & media_player.SUPPORT_VOLUME_SET:
|
2018-01-29 00:43:27 +00:00
|
|
|
yield _AlexaSpeaker(self.entity)
|
2018-01-26 05:06:57 +00:00
|
|
|
|
2018-02-06 00:02:08 +00:00
|
|
|
step_volume_features = (media_player.SUPPORT_VOLUME_MUTE |
|
|
|
|
media_player.SUPPORT_VOLUME_STEP)
|
|
|
|
if supported & step_volume_features:
|
|
|
|
yield _AlexaStepSpeaker(self.entity)
|
|
|
|
|
2018-01-26 05:06:57 +00:00
|
|
|
playback_features = (media_player.SUPPORT_PLAY |
|
|
|
|
media_player.SUPPORT_PAUSE |
|
|
|
|
media_player.SUPPORT_STOP |
|
|
|
|
media_player.SUPPORT_NEXT_TRACK |
|
|
|
|
media_player.SUPPORT_PREVIOUS_TRACK)
|
|
|
|
if supported & playback_features:
|
2018-01-29 00:43:27 +00:00
|
|
|
yield _AlexaPlaybackController(self.entity)
|
2018-01-26 05:06:57 +00:00
|
|
|
|
2018-01-29 06:22:04 +00:00
|
|
|
if supported & media_player.SUPPORT_SELECT_SOURCE:
|
|
|
|
yield _AlexaInputController(self.entity)
|
|
|
|
|
2018-01-26 05:06:57 +00:00
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
@ENTITY_ADAPTERS.register(scene.DOMAIN)
|
|
|
|
class _SceneCapabilities(_AlexaEntity):
|
|
|
|
def description(self):
|
|
|
|
# Required description as per Amazon Scene docs
|
|
|
|
scene_fmt = '{} (Scene connected via Home Assistant)'
|
|
|
|
return scene_fmt.format(_AlexaEntity.description(self))
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
def default_display_categories(self):
|
|
|
|
return [_DisplayCategory.SCENE_TRIGGER]
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def interfaces(self):
|
|
|
|
return [_AlexaSceneController(self.entity,
|
|
|
|
supports_deactivation=False)]
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
@ENTITY_ADAPTERS.register(script.DOMAIN)
|
|
|
|
class _ScriptCapabilities(_AlexaEntity):
|
2018-01-26 05:06:57 +00:00
|
|
|
def default_display_categories(self):
|
|
|
|
return [_DisplayCategory.ACTIVITY_TRIGGER]
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def interfaces(self):
|
2018-01-26 05:06:57 +00:00
|
|
|
can_cancel = bool(self.entity.attributes.get('can_cancel'))
|
2018-01-29 00:43:27 +00:00
|
|
|
return [_AlexaSceneController(self.entity,
|
|
|
|
supports_deactivation=can_cancel)]
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
@ENTITY_ADAPTERS.register(group.DOMAIN)
|
|
|
|
class _GroupCapabilities(_AlexaEntity):
|
2018-01-26 05:06:57 +00:00
|
|
|
def default_display_categories(self):
|
|
|
|
return [_DisplayCategory.SCENE_TRIGGER]
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def interfaces(self):
|
|
|
|
return [_AlexaSceneController(self.entity,
|
|
|
|
supports_deactivation=True)]
|
2018-01-26 05:06:57 +00:00
|
|
|
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
|
|
|
|
class _SensorCapabilities(_AlexaEntity):
|
2018-01-26 18:40:39 +00:00
|
|
|
def default_display_categories(self):
|
|
|
|
# although there are other kinds of sensors, all but temperature
|
|
|
|
# sensors are currently ignored.
|
|
|
|
return [_DisplayCategory.TEMPERATURE_SENSOR]
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
def interfaces(self):
|
2018-01-26 18:40:39 +00:00
|
|
|
attrs = self.entity.attributes
|
|
|
|
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
|
|
|
|
TEMP_FAHRENHEIT,
|
|
|
|
TEMP_CELSIUS,
|
|
|
|
):
|
2018-01-29 00:43:27 +00:00
|
|
|
yield _AlexaTemperatureSensor(self.entity)
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
|
2018-01-23 08:01:18 +00:00
|
|
|
class _Cause(object):
|
|
|
|
"""Possible causes for property changes.
|
|
|
|
|
|
|
|
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Indicates that the event was caused by a customer interaction with an
|
|
|
|
# application. For example, a customer switches on a light, or locks a door
|
|
|
|
# using the Alexa app or an app provided by a device vendor.
|
|
|
|
APP_INTERACTION = 'APP_INTERACTION'
|
|
|
|
|
|
|
|
# Indicates that the event was caused by a physical interaction with an
|
|
|
|
# endpoint. For example manually switching on a light or manually locking a
|
|
|
|
# door lock
|
|
|
|
PHYSICAL_INTERACTION = 'PHYSICAL_INTERACTION'
|
|
|
|
|
|
|
|
# Indicates that the event was caused by the periodic poll of an appliance,
|
|
|
|
# which found a change in value. For example, you might poll a temperature
|
|
|
|
# sensor every hour, and send the updated temperature to Alexa.
|
|
|
|
PERIODIC_POLL = 'PERIODIC_POLL'
|
|
|
|
|
|
|
|
# Indicates that the event was caused by the application of a device rule.
|
|
|
|
# For example, a customer configures a rule to switch on a light if a
|
|
|
|
# motion sensor detects motion. In this case, Alexa receives an event from
|
|
|
|
# the motion sensor, and another event from the light to indicate that its
|
|
|
|
# state change was caused by the rule.
|
|
|
|
RULE_TRIGGER = 'RULE_TRIGGER'
|
|
|
|
|
|
|
|
# Indicates that the event was caused by a voice interaction with Alexa.
|
|
|
|
# For example a user speaking to their Echo device.
|
|
|
|
VOICE_INTERACTION = 'VOICE_INTERACTION'
|
|
|
|
|
|
|
|
|
2018-01-05 20:33:22 +00:00
|
|
|
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 {}
|
2017-11-18 05:10:24 +00:00
|
|
|
|
|
|
|
|
2018-01-23 18:45:28 +00:00
|
|
|
@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)
|
2018-01-26 05:06:57 +00:00
|
|
|
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
|
2018-01-23 18:45:28 +00:00
|
|
|
return b'' if response is None else self.json(response)
|
|
|
|
|
|
|
|
|
2017-09-16 19:35:28 +00:00
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_handle_message(hass, config, 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)
|
|
|
|
|
2017-11-18 05:10:24 +00:00
|
|
|
return (yield from funct_ref(hass, config, message))
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
|
2018-01-26 18:40:39 +00:00
|
|
|
def api_message(request,
|
|
|
|
name='Response',
|
|
|
|
namespace='Alexa',
|
|
|
|
payload=None,
|
|
|
|
context=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
|
|
|
}
|
|
|
|
|
2018-01-29 22:37:19 +00:00
|
|
|
# If a correlation token exists, add it to header / Need by Async requests
|
2017-10-07 20:31:57 +00:00
|
|
|
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
|
|
|
|
2018-01-26 18:40:39 +00:00
|
|
|
if context is not None:
|
|
|
|
response[API_CONTEXT] = context
|
|
|
|
|
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
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_discovery(hass, config, 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():
|
2018-01-05 20:33:22 +00:00
|
|
|
if not config.should_expose(entity.entity_id):
|
2017-11-18 05:10:24 +00:00
|
|
|
_LOGGER.debug("Not exposing %s because filtered by config",
|
|
|
|
entity.entity_id)
|
|
|
|
continue
|
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
if entity.domain not in ENTITY_ADAPTERS:
|
2017-09-16 19:35:28 +00:00
|
|
|
continue
|
2018-01-29 00:43:27 +00:00
|
|
|
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
2017-10-07 20:31:57 +00:00
|
|
|
endpoint = {
|
2018-01-29 00:43:27 +00:00
|
|
|
'displayCategories': alexa_entity.display_categories(),
|
2017-09-16 19:35:28 +00:00
|
|
|
'additionalApplianceDetails': {},
|
2018-01-29 00:43:27 +00:00
|
|
|
'endpointId': alexa_entity.entity_id(),
|
|
|
|
'friendlyName': alexa_entity.friendly_name(),
|
|
|
|
'description': alexa_entity.description(),
|
2017-11-17 17:14:22 +00:00
|
|
|
'manufacturerName': 'Home Assistant',
|
2017-09-16 19:35:28 +00:00
|
|
|
}
|
2018-01-26 05:06:57 +00:00
|
|
|
|
2018-01-29 00:43:27 +00:00
|
|
|
endpoint['capabilities'] = [
|
|
|
|
i.serialize_discovery() for i in alexa_entity.interfaces()]
|
|
|
|
|
|
|
|
if not endpoint['capabilities']:
|
2018-01-26 18:40:39 +00:00
|
|
|
_LOGGER.debug("Not exposing %s because it has no capabilities",
|
|
|
|
entity.entity_id)
|
|
|
|
continue
|
2017-10-07 20:31:57 +00:00
|
|
|
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):
|
2018-01-21 06:35:38 +00:00
|
|
|
"""Decorate for extract entity object from request."""
|
2017-09-16 19:35:28 +00:00
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_entity_wrapper(hass, config, request):
|
2017-09-16 19:35:28 +00:00
|
|
|
"""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
|
|
|
|
2017-11-18 05:10:24 +00:00
|
|
|
return (yield from funct(hass, config, request, entity))
|
2017-09-16 19:35:28 +00:00
|
|
|
|
|
|
|
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
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_turn_on(hass, config, request, entity):
|
2017-09-16 19:35:28 +00:00
|
|
|
"""Process a turn on request."""
|
2017-11-18 05:10:24 +00:00
|
|
|
domain = entity.domain
|
|
|
|
|
2017-12-24 23:05:56 +00:00
|
|
|
service = SERVICE_TURN_ON
|
|
|
|
if entity.domain == cover.DOMAIN:
|
|
|
|
service = cover.SERVICE_OPEN_COVER
|
|
|
|
|
|
|
|
yield from hass.services.async_call(domain, service, {
|
2017-09-16 19:35:28 +00:00
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
2017-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-09-16 19:35:28 +00:00
|
|
|
|
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
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_turn_off(hass, config, request, entity):
|
2017-09-16 19:35:28 +00:00
|
|
|
"""Process a turn off request."""
|
2017-11-18 05:10:24 +00:00
|
|
|
domain = entity.domain
|
|
|
|
if entity.domain == group.DOMAIN:
|
|
|
|
domain = ha.DOMAIN
|
|
|
|
|
2017-12-24 23:05:56 +00:00
|
|
|
service = SERVICE_TURN_OFF
|
|
|
|
if entity.domain == cover.DOMAIN:
|
|
|
|
service = cover.SERVICE_CLOSE_COVER
|
|
|
|
|
|
|
|
yield from hass.services.async_call(domain, service, {
|
2017-09-16 19:35:28 +00:00
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
2017-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-09-16 19:35:28 +00:00
|
|
|
|
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-11-18 05:10:24 +00:00
|
|
|
def async_api_set_brightness(hass, config, request, entity):
|
2017-10-07 20:31:57 +00:00
|
|
|
"""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,
|
2017-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-11-01 03:28:17 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_adjust_brightness(hass, config, request, entity):
|
2018-01-27 19:58:27 +00:00
|
|
|
"""Process an adjust brightness request."""
|
2017-11-01 03:28:17 +00:00
|
|
|
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:
|
2017-11-01 11:16:05 +00:00
|
|
|
current = 0
|
2017-11-01 03:28:17 +00:00
|
|
|
|
|
|
|
# set brightness
|
2017-11-01 11:16:05 +00:00
|
|
|
brightness = max(0, brightness_delta + current)
|
2017-11-01 03:28:17 +00:00
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
light.ATTR_BRIGHTNESS_PCT: brightness,
|
2017-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-11-01 03:28:17 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_set_color(hass, config, request, entity):
|
2017-11-01 03:28:17 +00:00
|
|
|
"""Process a set color request."""
|
2017-11-01 11:16:05 +00:00
|
|
|
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
|
|
|
|
rgb = color_util.color_hsb_to_RGB(
|
|
|
|
float(request[API_PAYLOAD]['color']['hue']),
|
|
|
|
float(request[API_PAYLOAD]['color']['saturation']),
|
|
|
|
float(request[API_PAYLOAD]['color']['brightness'])
|
|
|
|
)
|
|
|
|
|
|
|
|
if supported & light.SUPPORT_RGB_COLOR > 0:
|
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
light.ATTR_RGB_COLOR: rgb,
|
2017-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-11-01 11:16:05 +00:00
|
|
|
else:
|
|
|
|
xyz = color_util.color_RGB_to_xy(*rgb)
|
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
|
|
|
|
light.ATTR_BRIGHTNESS: xyz[2],
|
2017-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-11-01 03:28:17 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_set_color_temperature(hass, config, request, entity):
|
2017-11-01 03:28:17 +00:00
|
|
|
"""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,
|
2017-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-11-01 03:28:17 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(
|
|
|
|
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_decrease_color_temp(hass, config, request, entity):
|
2017-11-01 03:28:17 +00:00
|
|
|
"""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,
|
2017-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-11-01 03:28:17 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(
|
|
|
|
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_increase_color_temp(hass, config, request, entity):
|
2018-01-26 05:06:57 +00:00
|
|
|
"""Process an increase color temperature request."""
|
2017-11-01 03:28:17 +00:00
|
|
|
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-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-10-07 20:31:57 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.SceneController', 'Activate'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_activate(hass, config, request, entity):
|
2018-01-26 05:06:57 +00:00
|
|
|
"""Process an activate request."""
|
|
|
|
if entity.domain == group.DOMAIN:
|
|
|
|
domain = ha.DOMAIN
|
|
|
|
else:
|
|
|
|
domain = entity.domain
|
|
|
|
|
|
|
|
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
|
2017-11-17 17:14:22 +00:00
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
2017-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
2018-01-23 08:01:18 +00:00
|
|
|
payload = {
|
|
|
|
'cause': {'type': _Cause.VOICE_INTERACTION},
|
|
|
|
'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
|
|
|
|
}
|
|
|
|
|
|
|
|
return api_message(
|
|
|
|
request,
|
|
|
|
name='ActivationStarted',
|
|
|
|
namespace='Alexa.SceneController',
|
|
|
|
payload=payload,
|
|
|
|
)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
|
2018-01-26 05:06:57 +00:00
|
|
|
@HANDLERS.register(('Alexa.SceneController', 'Deactivate'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_deactivate(hass, config, request, entity):
|
|
|
|
"""Process a deactivate request."""
|
|
|
|
if entity.domain == group.DOMAIN:
|
|
|
|
domain = ha.DOMAIN
|
|
|
|
else:
|
|
|
|
domain = entity.domain
|
|
|
|
|
|
|
|
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
|
|
|
}, blocking=False)
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
'cause': {'type': _Cause.VOICE_INTERACTION},
|
|
|
|
'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
|
|
|
|
}
|
|
|
|
|
|
|
|
return api_message(
|
|
|
|
request,
|
|
|
|
name='DeactivationStarted',
|
|
|
|
namespace='Alexa.SceneController',
|
|
|
|
payload=payload,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2017-11-17 17:14:22 +00:00
|
|
|
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_set_percentage(hass, config, request, entity):
|
2017-11-17 17:14:22 +00:00
|
|
|
"""Process a set percentage request."""
|
|
|
|
percentage = int(request[API_PAYLOAD]['percentage'])
|
|
|
|
service = None
|
|
|
|
data = {ATTR_ENTITY_ID: entity.entity_id}
|
|
|
|
|
|
|
|
if entity.domain == fan.DOMAIN:
|
|
|
|
service = fan.SERVICE_SET_SPEED
|
|
|
|
speed = "off"
|
|
|
|
|
|
|
|
if percentage <= 33:
|
|
|
|
speed = "low"
|
|
|
|
elif percentage <= 66:
|
|
|
|
speed = "medium"
|
|
|
|
elif percentage <= 100:
|
|
|
|
speed = "high"
|
|
|
|
data[fan.ATTR_SPEED] = speed
|
|
|
|
|
|
|
|
elif entity.domain == cover.DOMAIN:
|
|
|
|
service = SERVICE_SET_COVER_POSITION
|
|
|
|
data[cover.ATTR_POSITION] = percentage
|
|
|
|
|
2017-12-29 17:44:06 +00:00
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, service, data, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_adjust_percentage(hass, config, request, entity):
|
2018-01-27 19:58:27 +00:00
|
|
|
"""Process an adjust percentage request."""
|
2017-11-17 17:14:22 +00:00
|
|
|
percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
|
|
|
|
service = None
|
|
|
|
data = {ATTR_ENTITY_ID: entity.entity_id}
|
|
|
|
|
|
|
|
if entity.domain == fan.DOMAIN:
|
|
|
|
service = fan.SERVICE_SET_SPEED
|
|
|
|
speed = entity.attributes.get(fan.ATTR_SPEED)
|
|
|
|
|
|
|
|
if speed == "off":
|
|
|
|
current = 0
|
|
|
|
elif speed == "low":
|
|
|
|
current = 33
|
|
|
|
elif speed == "medium":
|
|
|
|
current = 66
|
|
|
|
elif speed == "high":
|
|
|
|
current = 100
|
|
|
|
|
|
|
|
# set percentage
|
|
|
|
percentage = max(0, percentage_delta + current)
|
|
|
|
speed = "off"
|
|
|
|
|
|
|
|
if percentage <= 33:
|
|
|
|
speed = "low"
|
|
|
|
elif percentage <= 66:
|
|
|
|
speed = "medium"
|
|
|
|
elif percentage <= 100:
|
|
|
|
speed = "high"
|
|
|
|
|
|
|
|
data[fan.ATTR_SPEED] = speed
|
|
|
|
|
|
|
|
elif entity.domain == cover.DOMAIN:
|
|
|
|
service = SERVICE_SET_COVER_POSITION
|
|
|
|
|
|
|
|
current = entity.attributes.get(cover.ATTR_POSITION)
|
|
|
|
|
|
|
|
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
|
|
|
|
|
2017-12-29 17:44:06 +00:00
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, service, data, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.LockController', 'Lock'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_lock(hass, config, request, entity):
|
2017-11-17 17:14:22 +00:00
|
|
|
"""Process a lock request."""
|
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
2017-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
# Not supported by Alexa yet
|
|
|
|
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_unlock(hass, config, request, entity):
|
2018-01-27 19:58:27 +00:00
|
|
|
"""Process an unlock request."""
|
2017-11-17 17:14:22 +00:00
|
|
|
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
2017-12-29 17:44:06 +00:00
|
|
|
}, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_set_volume(hass, config, request, entity):
|
2017-11-17 17:14:22 +00:00
|
|
|
"""Process a set volume request."""
|
|
|
|
volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
|
|
|
|
|
|
|
|
data = {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
|
|
|
}
|
|
|
|
|
2017-12-29 17:44:06 +00:00
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, SERVICE_VOLUME_SET,
|
|
|
|
data, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
2018-01-29 06:22:04 +00:00
|
|
|
@HANDLERS.register(('Alexa.InputController', 'SelectInput'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_select_input(hass, config, request, entity):
|
|
|
|
"""Process a set input request."""
|
|
|
|
media_input = request[API_PAYLOAD]['input']
|
|
|
|
|
|
|
|
# attempt to map the ALL UPPERCASE payload name to a source
|
|
|
|
source_list = entity.attributes[media_player.ATTR_INPUT_SOURCE_LIST] or []
|
|
|
|
for source in source_list:
|
|
|
|
# response will always be space separated, so format the source in the
|
|
|
|
# most likely way to find a match
|
|
|
|
formatted_source = source.lower().replace('-', ' ').replace('_', ' ')
|
|
|
|
if formatted_source in media_input.lower():
|
|
|
|
media_input = source
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
msg = 'failed to map input {} to a media source on {}'.format(
|
|
|
|
media_input, entity.entity_id)
|
|
|
|
_LOGGER.error(msg)
|
|
|
|
return api_error(
|
|
|
|
request, error_type='INVALID_VALUE', error_message=msg)
|
|
|
|
|
|
|
|
data = {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
media_player.ATTR_INPUT_SOURCE: media_input,
|
|
|
|
}
|
|
|
|
|
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, media_player.SERVICE_SELECT_SOURCE,
|
|
|
|
data, blocking=False)
|
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
2017-11-17 17:14:22 +00:00
|
|
|
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_adjust_volume(hass, config, request, entity):
|
2018-01-27 19:58:27 +00:00
|
|
|
"""Process an adjust volume request."""
|
2017-11-17 17:14:22 +00:00
|
|
|
volume_delta = int(request[API_PAYLOAD]['volume'])
|
|
|
|
|
|
|
|
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
|
|
|
|
|
|
|
# read current state
|
|
|
|
try:
|
|
|
|
current = math.floor(int(current_level * 100))
|
|
|
|
except ZeroDivisionError:
|
|
|
|
current = 0
|
|
|
|
|
|
|
|
volume = float(max(0, volume_delta + current) / 100)
|
|
|
|
|
|
|
|
data = {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
|
|
|
}
|
|
|
|
|
2017-12-29 17:44:06 +00:00
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, media_player.SERVICE_VOLUME_SET,
|
|
|
|
data, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
2018-02-06 00:02:08 +00:00
|
|
|
@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_adjust_volume_step(hass, config, request, entity):
|
|
|
|
"""Process an adjust volume step request."""
|
2018-02-11 19:25:05 +00:00
|
|
|
volume_step = round(float(request[API_PAYLOAD]['volumeSteps'] / 100), 2)
|
2018-02-06 00:02:08 +00:00
|
|
|
|
|
|
|
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
|
|
|
|
|
|
|
volume = current_level + volume_step
|
|
|
|
|
|
|
|
data = {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
|
|
|
}
|
|
|
|
|
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, media_player.SERVICE_VOLUME_SET,
|
|
|
|
data, blocking=False)
|
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute'))
|
2017-11-17 17:14:22 +00:00
|
|
|
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_set_mute(hass, config, request, entity):
|
2017-11-17 17:14:22 +00:00
|
|
|
"""Process a set mute request."""
|
|
|
|
mute = bool(request[API_PAYLOAD]['mute'])
|
|
|
|
|
|
|
|
data = {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id,
|
|
|
|
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
|
|
|
|
}
|
|
|
|
|
2017-12-29 17:44:06 +00:00
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, media_player.SERVICE_VOLUME_MUTE,
|
|
|
|
data, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_play(hass, config, request, entity):
|
2017-11-17 17:14:22 +00:00
|
|
|
"""Process a play request."""
|
|
|
|
data = {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
|
|
|
}
|
|
|
|
|
2017-12-29 17:44:06 +00:00
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, SERVICE_MEDIA_PLAY,
|
|
|
|
data, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_pause(hass, config, request, entity):
|
2017-11-17 17:14:22 +00:00
|
|
|
"""Process a pause request."""
|
|
|
|
data = {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
|
|
|
}
|
|
|
|
|
2017-12-29 17:44:06 +00:00
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, SERVICE_MEDIA_PAUSE,
|
|
|
|
data, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_stop(hass, config, request, entity):
|
2017-11-17 17:14:22 +00:00
|
|
|
"""Process a stop request."""
|
|
|
|
data = {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
|
|
|
}
|
|
|
|
|
2017-12-29 17:44:06 +00:00
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, SERVICE_MEDIA_STOP,
|
|
|
|
data, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_next(hass, config, request, entity):
|
2017-11-17 17:14:22 +00:00
|
|
|
"""Process a next request."""
|
|
|
|
data = {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
|
|
|
}
|
|
|
|
|
2017-12-29 17:44:06 +00:00
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, SERVICE_MEDIA_NEXT_TRACK,
|
|
|
|
data, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
2017-11-18 05:10:24 +00:00
|
|
|
def async_api_previous(hass, config, request, entity):
|
2017-11-17 17:14:22 +00:00
|
|
|
"""Process a previous request."""
|
|
|
|
data = {
|
|
|
|
ATTR_ENTITY_ID: entity.entity_id
|
|
|
|
}
|
|
|
|
|
2017-12-29 17:44:06 +00:00
|
|
|
yield from hass.services.async_call(
|
|
|
|
entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
|
|
|
|
data, blocking=False)
|
2017-11-17 17:14:22 +00:00
|
|
|
|
|
|
|
return api_message(request)
|
2018-01-26 18:40:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register(('Alexa', 'ReportState'))
|
|
|
|
@extract_entity
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_api_reportstate(hass, config, request, entity):
|
|
|
|
"""Process a ReportState request."""
|
2018-01-29 00:43:27 +00:00
|
|
|
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
|
|
|
|
properties = []
|
|
|
|
for interface in alexa_entity.interfaces():
|
|
|
|
properties.extend(interface.serialize_properties())
|
2018-01-26 18:40:39 +00:00
|
|
|
|
|
|
|
return api_message(
|
|
|
|
request,
|
|
|
|
name='StateReport',
|
2018-01-29 00:43:27 +00:00
|
|
|
context={'properties': properties}
|
2018-01-26 18:40:39 +00:00
|
|
|
)
|