mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 17:57:11 +00:00
Refactor Alexa Smart Home API (#12016)
Having an object per interface will make it easier to support properties. Ideally, properties are reported in context in all responses. However current implementation reports them only in response to a ReportState request. This seems to work sufficiently. As long as the device is opened in the Alexa app, Amazon will poll the device state every few seconds with a ReportState request.
This commit is contained in:
parent
b3bf6c4be2
commit
84711aad90
@ -40,6 +40,7 @@ CONF_DESCRIPTION = 'description'
|
|||||||
CONF_DISPLAY_CATEGORIES = 'display_categories'
|
CONF_DISPLAY_CATEGORIES = 'display_categories'
|
||||||
|
|
||||||
HANDLERS = Registry()
|
HANDLERS = Registry()
|
||||||
|
ENTITY_ADAPTERS = Registry()
|
||||||
|
|
||||||
|
|
||||||
class _DisplayCategory(object):
|
class _DisplayCategory(object):
|
||||||
@ -133,10 +134,36 @@ def _capability(interface,
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class _EntityCapabilities(object):
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, config, entity):
|
def __init__(self, config, entity):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.entity = entity
|
self.entity = entity
|
||||||
|
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('.', '#')
|
||||||
|
|
||||||
def display_categories(self):
|
def display_categories(self):
|
||||||
"""Return a list of display categories."""
|
"""Return a list of display categories."""
|
||||||
@ -154,17 +181,166 @@ class _EntityCapabilities(object):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def capabilities(self):
|
def get_interface(self, capability):
|
||||||
"""Return a list of supported capabilities.
|
"""Return the given _AlexaInterface.
|
||||||
|
|
||||||
If the returned list is empty, the entity will not be discovered.
|
Raises _UnsupportedInterface.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
You might find _capability() useful.
|
def interfaces(self):
|
||||||
|
"""Return a list of supported interfaces.
|
||||||
|
|
||||||
|
Used for discovery. The list should contain _AlexaInterface instances.
|
||||||
|
If the list is empty, this entity will not be discovered.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class _GenericCapabilities(_EntityCapabilities):
|
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(),
|
||||||
|
},
|
||||||
|
# XXX this is incorrect, but the tests assert it
|
||||||
|
'retrievable': self.properties_retrievable(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaLockController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.LockController'
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaPlaybackController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.PlaybackController'
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
"""A generic, on/off device.
|
"""A generic, on/off device.
|
||||||
|
|
||||||
The choice of last resort.
|
The choice of last resort.
|
||||||
@ -173,78 +349,82 @@ class _GenericCapabilities(_EntityCapabilities):
|
|||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.OTHER]
|
return [_DisplayCategory.OTHER]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
return [_capability('Alexa.PowerController')]
|
return [_AlexaPowerController(self.entity)]
|
||||||
|
|
||||||
|
|
||||||
class _SwitchCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(switch.DOMAIN)
|
||||||
|
class _SwitchCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.SWITCH]
|
return [_DisplayCategory.SWITCH]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
return [_capability('Alexa.PowerController')]
|
return [_AlexaPowerController(self.entity)]
|
||||||
|
|
||||||
|
|
||||||
class _CoverCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
||||||
|
class _CoverCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.DOOR]
|
return [_DisplayCategory.DOOR]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
capabilities = [_capability('Alexa.PowerController')]
|
yield _AlexaPowerController(self.entity)
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & cover.SUPPORT_SET_POSITION:
|
if supported & cover.SUPPORT_SET_POSITION:
|
||||||
capabilities.append(_capability('Alexa.PercentageController'))
|
yield _AlexaPercentageController(self.entity)
|
||||||
return capabilities
|
|
||||||
|
|
||||||
|
|
||||||
class _LightCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(light.DOMAIN)
|
||||||
|
class _LightCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.LIGHT]
|
return [_DisplayCategory.LIGHT]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
capabilities = [_capability('Alexa.PowerController')]
|
yield _AlexaPowerController(self.entity)
|
||||||
|
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & light.SUPPORT_BRIGHTNESS:
|
if supported & light.SUPPORT_BRIGHTNESS:
|
||||||
capabilities.append(_capability('Alexa.BrightnessController'))
|
yield _AlexaBrightnessController(self.entity)
|
||||||
if supported & light.SUPPORT_RGB_COLOR:
|
if supported & light.SUPPORT_RGB_COLOR:
|
||||||
capabilities.append(_capability('Alexa.ColorController'))
|
yield _AlexaColorController(self.entity)
|
||||||
if supported & light.SUPPORT_XY_COLOR:
|
if supported & light.SUPPORT_XY_COLOR:
|
||||||
capabilities.append(_capability('Alexa.ColorController'))
|
yield _AlexaColorController(self.entity)
|
||||||
if supported & light.SUPPORT_COLOR_TEMP:
|
if supported & light.SUPPORT_COLOR_TEMP:
|
||||||
capabilities.append(
|
yield _AlexaColorTemperatureController(self.entity)
|
||||||
_capability('Alexa.ColorTemperatureController'))
|
|
||||||
return capabilities
|
|
||||||
|
|
||||||
|
|
||||||
class _FanCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(fan.DOMAIN)
|
||||||
|
class _FanCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.OTHER]
|
return [_DisplayCategory.OTHER]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
capabilities = [_capability('Alexa.PowerController')]
|
yield _AlexaPowerController(self.entity)
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & fan.SUPPORT_SET_SPEED:
|
if supported & fan.SUPPORT_SET_SPEED:
|
||||||
capabilities.append(_capability('Alexa.PercentageController'))
|
yield _AlexaPercentageController(self.entity)
|
||||||
return capabilities
|
|
||||||
|
|
||||||
|
|
||||||
class _LockCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(lock.DOMAIN)
|
||||||
|
class _LockCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.SMARTLOCK]
|
return [_DisplayCategory.SMARTLOCK]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
return [_capability('Alexa.LockController')]
|
return [_AlexaLockController(self.entity)]
|
||||||
|
|
||||||
|
|
||||||
class _MediaPlayerCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(media_player.DOMAIN)
|
||||||
|
class _MediaPlayerCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.TV]
|
return [_DisplayCategory.TV]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
capabilities = [_capability('Alexa.PowerController')]
|
yield _AlexaPowerController(self.entity)
|
||||||
|
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & media_player.SUPPORT_VOLUME_SET:
|
if supported & media_player.SUPPORT_VOLUME_SET:
|
||||||
capabilities.append(_capability('Alexa.Speaker'))
|
yield _AlexaSpeaker(self.entity)
|
||||||
|
|
||||||
playback_features = (media_player.SUPPORT_PLAY |
|
playback_features = (media_player.SUPPORT_PLAY |
|
||||||
media_player.SUPPORT_PAUSE |
|
media_player.SUPPORT_PAUSE |
|
||||||
@ -252,89 +432,59 @@ class _MediaPlayerCapabilities(_EntityCapabilities):
|
|||||||
media_player.SUPPORT_NEXT_TRACK |
|
media_player.SUPPORT_NEXT_TRACK |
|
||||||
media_player.SUPPORT_PREVIOUS_TRACK)
|
media_player.SUPPORT_PREVIOUS_TRACK)
|
||||||
if supported & playback_features:
|
if supported & playback_features:
|
||||||
capabilities.append(_capability('Alexa.PlaybackController'))
|
yield _AlexaPlaybackController(self.entity)
|
||||||
|
|
||||||
return capabilities
|
|
||||||
|
|
||||||
|
|
||||||
class _SceneCapabilities(_EntityCapabilities):
|
@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))
|
||||||
|
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.SCENE_TRIGGER]
|
return [_DisplayCategory.SCENE_TRIGGER]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
return [_capability('Alexa.SceneController')]
|
return [_AlexaSceneController(self.entity,
|
||||||
|
supports_deactivation=False)]
|
||||||
|
|
||||||
|
|
||||||
class _ScriptCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(script.DOMAIN)
|
||||||
|
class _ScriptCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.ACTIVITY_TRIGGER]
|
return [_DisplayCategory.ACTIVITY_TRIGGER]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
can_cancel = bool(self.entity.attributes.get('can_cancel'))
|
can_cancel = bool(self.entity.attributes.get('can_cancel'))
|
||||||
return [_capability('Alexa.SceneController',
|
return [_AlexaSceneController(self.entity,
|
||||||
supports_deactivation=can_cancel)]
|
supports_deactivation=can_cancel)]
|
||||||
|
|
||||||
|
|
||||||
class _GroupCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(group.DOMAIN)
|
||||||
|
class _GroupCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
return [_DisplayCategory.SCENE_TRIGGER]
|
return [_DisplayCategory.SCENE_TRIGGER]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
return [_capability('Alexa.SceneController',
|
return [_AlexaSceneController(self.entity,
|
||||||
supports_deactivation=True)]
|
supports_deactivation=True)]
|
||||||
|
|
||||||
|
|
||||||
class _SensorCapabilities(_EntityCapabilities):
|
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
|
||||||
|
class _SensorCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
# although there are other kinds of sensors, all but temperature
|
# although there are other kinds of sensors, all but temperature
|
||||||
# sensors are currently ignored.
|
# sensors are currently ignored.
|
||||||
return [_DisplayCategory.TEMPERATURE_SENSOR]
|
return [_DisplayCategory.TEMPERATURE_SENSOR]
|
||||||
|
|
||||||
def capabilities(self):
|
def interfaces(self):
|
||||||
capabilities = []
|
|
||||||
|
|
||||||
attrs = self.entity.attributes
|
attrs = self.entity.attributes
|
||||||
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
|
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
):
|
):
|
||||||
capabilities.append(_capability(
|
yield _AlexaTemperatureSensor(self.entity)
|
||||||
'Alexa.TemperatureSensor',
|
|
||||||
retrievable=True,
|
|
||||||
properties_supported=[{'name': 'temperature'}]))
|
|
||||||
|
|
||||||
return capabilities
|
|
||||||
|
|
||||||
|
|
||||||
class _UnknownEntityDomainError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _capabilities_for_entity(config, entity):
|
|
||||||
"""Return an _EntityCapabilities appropriate for given entity.
|
|
||||||
|
|
||||||
raises _UnknownEntityDomainError if the given domain is unsupported.
|
|
||||||
"""
|
|
||||||
if entity.domain not in _CAPABILITIES_FOR_DOMAIN:
|
|
||||||
raise _UnknownEntityDomainError()
|
|
||||||
return _CAPABILITIES_FOR_DOMAIN[entity.domain](config, entity)
|
|
||||||
|
|
||||||
|
|
||||||
_CAPABILITIES_FOR_DOMAIN = {
|
|
||||||
alert.DOMAIN: _GenericCapabilities,
|
|
||||||
automation.DOMAIN: _GenericCapabilities,
|
|
||||||
cover.DOMAIN: _CoverCapabilities,
|
|
||||||
fan.DOMAIN: _FanCapabilities,
|
|
||||||
group.DOMAIN: _GroupCapabilities,
|
|
||||||
input_boolean.DOMAIN: _GenericCapabilities,
|
|
||||||
light.DOMAIN: _LightCapabilities,
|
|
||||||
lock.DOMAIN: _LockCapabilities,
|
|
||||||
media_player.DOMAIN: _MediaPlayerCapabilities,
|
|
||||||
scene.DOMAIN: _SceneCapabilities,
|
|
||||||
script.DOMAIN: _ScriptCapabilities,
|
|
||||||
switch.DOMAIN: _SwitchCapabilities,
|
|
||||||
sensor.DOMAIN: _SensorCapabilities,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class _Cause(object):
|
class _Cause(object):
|
||||||
@ -511,36 +661,26 @@ def async_api_discovery(hass, config, request):
|
|||||||
entity.entity_id)
|
entity.entity_id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
if entity.domain not in ENTITY_ADAPTERS:
|
||||||
entity_capabilities = _capabilities_for_entity(config, entity)
|
|
||||||
except _UnknownEntityDomainError:
|
|
||||||
continue
|
continue
|
||||||
|
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
|
||||||
entity_conf = config.entity_config.get(entity.entity_id, {})
|
|
||||||
|
|
||||||
friendly_name = entity_conf.get(CONF_NAME, entity.name)
|
|
||||||
description = entity_conf.get(CONF_DESCRIPTION, entity.entity_id)
|
|
||||||
|
|
||||||
# Required description as per Amazon Scene docs
|
|
||||||
if entity.domain == scene.DOMAIN:
|
|
||||||
scene_fmt = '{} (Scene connected via Home Assistant)'
|
|
||||||
description = scene_fmt.format(description)
|
|
||||||
|
|
||||||
endpoint = {
|
endpoint = {
|
||||||
'displayCategories': entity_capabilities.display_categories(),
|
'displayCategories': alexa_entity.display_categories(),
|
||||||
'additionalApplianceDetails': {},
|
'additionalApplianceDetails': {},
|
||||||
'endpointId': entity.entity_id.replace('.', '#'),
|
'endpointId': alexa_entity.entity_id(),
|
||||||
'friendlyName': friendly_name,
|
'friendlyName': alexa_entity.friendly_name(),
|
||||||
'description': description,
|
'description': alexa_entity.description(),
|
||||||
'manufacturerName': 'Home Assistant',
|
'manufacturerName': 'Home Assistant',
|
||||||
}
|
}
|
||||||
|
|
||||||
alexa_capabilities = entity_capabilities.capabilities()
|
endpoint['capabilities'] = [
|
||||||
if not alexa_capabilities:
|
i.serialize_discovery() for i in alexa_entity.interfaces()]
|
||||||
|
|
||||||
|
if not endpoint['capabilities']:
|
||||||
_LOGGER.debug("Not exposing %s because it has no capabilities",
|
_LOGGER.debug("Not exposing %s because it has no capabilities",
|
||||||
entity.entity_id)
|
entity.entity_id)
|
||||||
continue
|
continue
|
||||||
endpoint['capabilities'] = alexa_capabilities
|
|
||||||
discovery_endpoints.append(endpoint)
|
discovery_endpoints.append(endpoint)
|
||||||
|
|
||||||
return api_message(
|
return api_message(
|
||||||
@ -1033,18 +1173,13 @@ def async_api_previous(hass, config, request, entity):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_api_reportstate(hass, config, request, entity):
|
def async_api_reportstate(hass, config, request, entity):
|
||||||
"""Process a ReportState request."""
|
"""Process a ReportState request."""
|
||||||
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
|
||||||
temp_property = {
|
properties = []
|
||||||
'namespace': 'Alexa.TemperatureSensor',
|
for interface in alexa_entity.interfaces():
|
||||||
'name': 'temperature',
|
properties.extend(interface.serialize_properties())
|
||||||
'value': {
|
|
||||||
'value': float(entity.state),
|
|
||||||
'scale': API_TEMP_UNITS[unit],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return api_message(
|
return api_message(
|
||||||
request,
|
request,
|
||||||
name='StateReport',
|
name='StateReport',
|
||||||
context={'properties': [temp_property]}
|
context={'properties': properties}
|
||||||
)
|
)
|
||||||
|
@ -187,10 +187,32 @@ def test_discovery_request(hass):
|
|||||||
assert 'event' in msg
|
assert 'event' in msg
|
||||||
msg = msg['event']
|
msg = msg['event']
|
||||||
|
|
||||||
assert len(msg['payload']['endpoints']) == 17
|
|
||||||
assert msg['header']['name'] == 'Discover.Response'
|
assert msg['header']['name'] == 'Discover.Response'
|
||||||
assert msg['header']['namespace'] == 'Alexa.Discovery'
|
assert msg['header']['namespace'] == 'Alexa.Discovery'
|
||||||
|
|
||||||
|
endpoint_ids = set(
|
||||||
|
appliance['endpointId']
|
||||||
|
for appliance in msg['payload']['endpoints'])
|
||||||
|
assert endpoint_ids == {
|
||||||
|
'switch#test',
|
||||||
|
'light#test_1',
|
||||||
|
'light#test_2',
|
||||||
|
'light#test_3',
|
||||||
|
'script#test',
|
||||||
|
'script#test_2',
|
||||||
|
'input_boolean#test',
|
||||||
|
'scene#test',
|
||||||
|
'fan#test_1',
|
||||||
|
'fan#test_2',
|
||||||
|
'lock#test',
|
||||||
|
'media_player#test',
|
||||||
|
'alert#test',
|
||||||
|
'automation#test',
|
||||||
|
'group#test',
|
||||||
|
'cover#test',
|
||||||
|
'sensor#test_temp',
|
||||||
|
}
|
||||||
|
|
||||||
for appliance in msg['payload']['endpoints']:
|
for appliance in msg['payload']['endpoints']:
|
||||||
if appliance['endpointId'] == 'switch#test':
|
if appliance['endpointId'] == 'switch#test':
|
||||||
assert appliance['displayCategories'][0] == "SWITCH"
|
assert appliance['displayCategories'][0] == "SWITCH"
|
||||||
@ -267,6 +289,8 @@ def test_discovery_request(hass):
|
|||||||
assert len(appliance['capabilities']) == 1
|
assert len(appliance['capabilities']) == 1
|
||||||
assert appliance['capabilities'][-1]['interface'] == \
|
assert appliance['capabilities'][-1]['interface'] == \
|
||||||
'Alexa.SceneController'
|
'Alexa.SceneController'
|
||||||
|
capability = appliance['capabilities'][-1]
|
||||||
|
assert not capability['supportsDeactivation']
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if appliance['endpointId'] == 'fan#test_1':
|
if appliance['endpointId'] == 'fan#test_1':
|
||||||
@ -333,7 +357,7 @@ def test_discovery_request(hass):
|
|||||||
assert len(appliance['capabilities']) == 1
|
assert len(appliance['capabilities']) == 1
|
||||||
capability = appliance['capabilities'][-1]
|
capability = appliance['capabilities'][-1]
|
||||||
assert capability['interface'] == 'Alexa.SceneController'
|
assert capability['interface'] == 'Alexa.SceneController'
|
||||||
assert capability['supportsDeactivation'] is True
|
assert capability['supportsDeactivation']
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if appliance['endpointId'] == 'cover#test':
|
if appliance['endpointId'] == 'cover#test':
|
||||||
|
Loading…
x
Reference in New Issue
Block a user