mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Refactor Alexa capabilityResources object into class, Implement Alexa semantics object (#29917)
* Refactor capabilityResources object into class. Implement semantics object to support open, close, raise, lower utterences. Replace covers PercentageController with RangeController. Add semantics for covers. Remove PowerController for covers. Add new display categories. Add new items to Alexa Global Catalog. Implement garage door voice PIN code support though Alexa app. Fixed bug with getting property for ModeController. Fixed bug were PercentageController AdjustPercentage would exceed 100. * Comment fixes in Tests. * Reorder imports. * Added additional tests for more code coverage. * Added and additional test for more code coverage. * Explicitly return None for configuration() if not instance of AlexaCapabilityResource.
This commit is contained in:
parent
9804fbb527
commit
5baaa852dd
@ -13,11 +13,9 @@ from homeassistant.const import (
|
|||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||||
STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_ARMED_HOME,
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
STATE_CLOSED,
|
|
||||||
STATE_LOCKED,
|
STATE_LOCKED,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
STATE_OPEN,
|
|
||||||
STATE_PAUSED,
|
STATE_PAUSED,
|
||||||
STATE_PLAYING,
|
STATE_PLAYING,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
@ -34,10 +32,16 @@ from .const import (
|
|||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
PERCENTAGE_FAN_MAP,
|
PERCENTAGE_FAN_MAP,
|
||||||
RANGE_FAN_MAP,
|
RANGE_FAN_MAP,
|
||||||
Catalog,
|
|
||||||
Inputs,
|
Inputs,
|
||||||
)
|
)
|
||||||
from .errors import UnsupportedProperty
|
from .errors import UnsupportedProperty
|
||||||
|
from .resources import (
|
||||||
|
AlexaCapabilityResource,
|
||||||
|
AlexaGlobalCatalog,
|
||||||
|
AlexaModeResource,
|
||||||
|
AlexaPresetResource,
|
||||||
|
AlexaSemantics,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -108,12 +112,15 @@ class AlexaCapability:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def capability_resources():
|
def capability_resources():
|
||||||
"""Applicable to ToggleController, RangeController, and ModeController interfaces."""
|
"""Return the capability object.
|
||||||
|
|
||||||
|
Applicable to ToggleController, RangeController, and ModeController interfaces.
|
||||||
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def configuration():
|
def configuration():
|
||||||
"""Return the Configuration object."""
|
"""Return the configuration object."""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -121,6 +128,14 @@ class AlexaCapability:
|
|||||||
"""Applicable only to media players."""
|
"""Applicable only to media players."""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def semantics():
|
||||||
|
"""Return the semantics object.
|
||||||
|
|
||||||
|
Applicable to ToggleController, RangeController, and ModeController interfaces.
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def supported_operations():
|
def supported_operations():
|
||||||
"""Return the supportedOperations object."""
|
"""Return the supportedOperations object."""
|
||||||
@ -130,6 +145,10 @@ class AlexaCapability:
|
|||||||
"""Serialize according to the Discovery API."""
|
"""Serialize according to the Discovery API."""
|
||||||
result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"}
|
result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"}
|
||||||
|
|
||||||
|
instance = self.instance
|
||||||
|
if instance is not None:
|
||||||
|
result["instance"] = instance
|
||||||
|
|
||||||
properties_supported = self.properties_supported()
|
properties_supported = self.properties_supported()
|
||||||
if properties_supported:
|
if properties_supported:
|
||||||
result["properties"] = {
|
result["properties"] = {
|
||||||
@ -138,22 +157,19 @@ class AlexaCapability:
|
|||||||
"retrievable": self.properties_retrievable(),
|
"retrievable": self.properties_retrievable(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# pylint: disable=assignment-from-none
|
|
||||||
proactively_reported = self.capability_proactively_reported()
|
proactively_reported = self.capability_proactively_reported()
|
||||||
if proactively_reported is not None:
|
if proactively_reported is not None:
|
||||||
result["proactivelyReported"] = proactively_reported
|
result["proactivelyReported"] = proactively_reported
|
||||||
|
|
||||||
# pylint: disable=assignment-from-none
|
|
||||||
non_controllable = self.properties_non_controllable()
|
non_controllable = self.properties_non_controllable()
|
||||||
if non_controllable is not None:
|
if non_controllable is not None:
|
||||||
result["properties"]["nonControllable"] = non_controllable
|
result["properties"]["nonControllable"] = non_controllable
|
||||||
|
|
||||||
# pylint: disable=assignment-from-none
|
|
||||||
supports_deactivation = self.supports_deactivation()
|
supports_deactivation = self.supports_deactivation()
|
||||||
if supports_deactivation is not None:
|
if supports_deactivation is not None:
|
||||||
result["supportsDeactivation"] = supports_deactivation
|
result["supportsDeactivation"] = supports_deactivation
|
||||||
|
|
||||||
capability_resources = self.serialize_capability_resources()
|
capability_resources = self.capability_resources()
|
||||||
if capability_resources:
|
if capability_resources:
|
||||||
result["capabilityResources"] = capability_resources
|
result["capabilityResources"] = capability_resources
|
||||||
|
|
||||||
@ -161,10 +177,9 @@ class AlexaCapability:
|
|||||||
if configuration:
|
if configuration:
|
||||||
result["configuration"] = configuration
|
result["configuration"] = configuration
|
||||||
|
|
||||||
# pylint: disable=assignment-from-none
|
semantics = self.semantics()
|
||||||
instance = self.instance
|
if semantics:
|
||||||
if instance is not None:
|
result["semantics"] = semantics
|
||||||
result["instance"] = instance
|
|
||||||
|
|
||||||
supported_operations = self.supported_operations()
|
supported_operations = self.supported_operations()
|
||||||
if supported_operations:
|
if supported_operations:
|
||||||
@ -196,36 +211,6 @@ class AlexaCapability:
|
|||||||
|
|
||||||
yield result
|
yield result
|
||||||
|
|
||||||
def serialize_capability_resources(self):
|
|
||||||
"""Return capabilityResources friendlyNames serialized for an API response."""
|
|
||||||
resources = self.capability_resources()
|
|
||||||
if resources:
|
|
||||||
return {"friendlyNames": self.serialize_friendly_names(resources)}
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def serialize_friendly_names(resources):
|
|
||||||
"""Return capabilityResources, ModeResources, or presetResources friendlyNames serialized for an API response."""
|
|
||||||
friendly_names = []
|
|
||||||
for resource in resources:
|
|
||||||
if resource["type"] == Catalog.LABEL_ASSET:
|
|
||||||
friendly_names.append(
|
|
||||||
{
|
|
||||||
"@type": Catalog.LABEL_ASSET,
|
|
||||||
"value": {"assetId": resource["value"]},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
friendly_names.append(
|
|
||||||
{
|
|
||||||
"@type": Catalog.LABEL_TEXT,
|
|
||||||
"value": {"text": resource["value"], "locale": "en-US"},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return friendly_names
|
|
||||||
|
|
||||||
|
|
||||||
class Alexa(AlexaCapability):
|
class Alexa(AlexaCapability):
|
||||||
"""Implements Alexa Interface.
|
"""Implements Alexa Interface.
|
||||||
@ -906,6 +891,8 @@ class AlexaModeController(AlexaCapability):
|
|||||||
def __init__(self, entity, instance, non_controllable=False):
|
def __init__(self, entity, instance, non_controllable=False):
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
super().__init__(entity, instance)
|
super().__init__(entity, instance)
|
||||||
|
self._resource = None
|
||||||
|
self._semantics = None
|
||||||
self.properties_non_controllable = lambda: non_controllable
|
self.properties_non_controllable = lambda: non_controllable
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -922,108 +909,102 @@ class AlexaModeController(AlexaCapability):
|
|||||||
|
|
||||||
def properties_retrievable(self):
|
def properties_retrievable(self):
|
||||||
"""Return True if properties can be retrieved."""
|
"""Return True if properties can be retrieved."""
|
||||||
|
return True
|
||||||
|
|
||||||
def get_property(self, name):
|
def get_property(self, name):
|
||||||
"""Read and return a property."""
|
"""Read and return a property."""
|
||||||
if name != "mode":
|
if name != "mode":
|
||||||
raise UnsupportedProperty(name)
|
raise UnsupportedProperty(name)
|
||||||
|
|
||||||
|
# Fan Direction
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||||
return self.entity.attributes.get(fan.ATTR_DIRECTION)
|
mode = self.entity.attributes.get(fan.ATTR_DIRECTION, None)
|
||||||
|
if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN):
|
||||||
|
return f"{fan.ATTR_DIRECTION}.{mode}"
|
||||||
|
|
||||||
|
# Cover Position
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
return self.entity.attributes.get(cover.ATTR_POSITION)
|
# Return state instead of position when using ModeController.
|
||||||
|
mode = self.entity.state
|
||||||
|
if mode in (
|
||||||
|
cover.STATE_OPEN,
|
||||||
|
cover.STATE_OPENING,
|
||||||
|
cover.STATE_CLOSED,
|
||||||
|
cover.STATE_CLOSING,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
):
|
||||||
|
return f"{cover.ATTR_POSITION}.{mode}"
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def configuration(self):
|
def configuration(self):
|
||||||
"""Return configuration with modeResources."""
|
"""Return configuration with modeResources."""
|
||||||
return self.serialize_mode_resources()
|
if isinstance(self._resource, AlexaCapabilityResource):
|
||||||
|
return self._resource.serialize_configuration()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def capability_resources(self):
|
def capability_resources(self):
|
||||||
"""Return capabilityResources object."""
|
"""Return capabilityResources object."""
|
||||||
capability_resources = []
|
|
||||||
|
|
||||||
|
# Fan Direction Resource
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||||
capability_resources = [
|
self._resource = AlexaModeResource(
|
||||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION}
|
[AlexaGlobalCatalog.SETTING_DIRECTION], False
|
||||||
]
|
)
|
||||||
|
self._resource.add_mode(
|
||||||
|
f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", [fan.DIRECTION_FORWARD]
|
||||||
|
)
|
||||||
|
self._resource.add_mode(
|
||||||
|
f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", [fan.DIRECTION_REVERSE]
|
||||||
|
)
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
|
# Cover Position Resources
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
capability_resources = [
|
self._resource = AlexaModeResource(
|
||||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_MODE},
|
["Position", AlexaGlobalCatalog.SETTING_OPENING], False
|
||||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_PRESET},
|
)
|
||||||
]
|
self._resource.add_mode(
|
||||||
|
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
|
||||||
|
[AlexaGlobalCatalog.VALUE_OPEN],
|
||||||
|
)
|
||||||
|
self._resource.add_mode(
|
||||||
|
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
|
||||||
|
[AlexaGlobalCatalog.VALUE_CLOSE],
|
||||||
|
)
|
||||||
|
self._resource.add_mode(f"{cover.ATTR_POSITION}.custom", ["Custom"])
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
return capability_resources
|
return None
|
||||||
|
|
||||||
def mode_resources(self):
|
def semantics(self):
|
||||||
"""Return modeResources object."""
|
"""Build and return semantics object."""
|
||||||
mode_resources = None
|
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
|
||||||
mode_resources = {
|
|
||||||
"ordered": False,
|
|
||||||
"resources": [
|
|
||||||
{
|
|
||||||
"value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}",
|
|
||||||
"friendly_names": [
|
|
||||||
{"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_FORWARD}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}",
|
|
||||||
"friendly_names": [
|
|
||||||
{"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_REVERSE}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
# Cover Position
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
mode_resources = {
|
self._semantics = AlexaSemantics()
|
||||||
"ordered": False,
|
self._semantics.add_action_to_directive(
|
||||||
"resources": [
|
[AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER],
|
||||||
{
|
"SetMode",
|
||||||
"value": f"{cover.ATTR_POSITION}.{STATE_OPEN}",
|
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
|
||||||
"friendly_names": [
|
)
|
||||||
{"type": Catalog.LABEL_TEXT, "value": "open"},
|
self._semantics.add_action_to_directive(
|
||||||
{"type": Catalog.LABEL_TEXT, "value": "opened"},
|
[AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE],
|
||||||
{"type": Catalog.LABEL_TEXT, "value": "raise"},
|
"SetMode",
|
||||||
{"type": Catalog.LABEL_TEXT, "value": "raised"},
|
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"},
|
||||||
],
|
)
|
||||||
},
|
self._semantics.add_states_to_value(
|
||||||
{
|
[AlexaSemantics.STATES_CLOSED],
|
||||||
"value": f"{cover.ATTR_POSITION}.{STATE_CLOSED}",
|
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
|
||||||
"friendly_names": [
|
)
|
||||||
{"type": Catalog.LABEL_TEXT, "value": "close"},
|
self._semantics.add_states_to_value(
|
||||||
{"type": Catalog.LABEL_TEXT, "value": "closed"},
|
[AlexaSemantics.STATES_OPEN],
|
||||||
{"type": Catalog.LABEL_TEXT, "value": "shut"},
|
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
|
||||||
{"type": Catalog.LABEL_TEXT, "value": "lower"},
|
)
|
||||||
{"type": Catalog.LABEL_TEXT, "value": "lowered"},
|
return self._semantics.serialize_semantics()
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
return mode_resources
|
return None
|
||||||
|
|
||||||
def serialize_mode_resources(self):
|
|
||||||
"""Return ModeResources, friendlyNames serialized for an API response."""
|
|
||||||
mode_resources = []
|
|
||||||
resources = self.mode_resources()
|
|
||||||
ordered = resources["ordered"]
|
|
||||||
for resource in resources["resources"]:
|
|
||||||
mode_value = resource["value"]
|
|
||||||
friendly_names = resource["friendly_names"]
|
|
||||||
result = {
|
|
||||||
"value": mode_value,
|
|
||||||
"modeResources": {
|
|
||||||
"friendlyNames": self.serialize_friendly_names(friendly_names)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mode_resources.append(result)
|
|
||||||
|
|
||||||
return {"ordered": ordered, "supportedModes": mode_resources}
|
|
||||||
|
|
||||||
|
|
||||||
class AlexaRangeController(AlexaCapability):
|
class AlexaRangeController(AlexaCapability):
|
||||||
@ -1035,6 +1016,8 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
def __init__(self, entity, instance, non_controllable=False):
|
def __init__(self, entity, instance, non_controllable=False):
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
super().__init__(entity, instance)
|
super().__init__(entity, instance)
|
||||||
|
self._resource = None
|
||||||
|
self._semantics = None
|
||||||
self.properties_non_controllable = lambda: non_controllable
|
self.properties_non_controllable = lambda: non_controllable
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -1058,88 +1041,111 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
if name != "rangeValue":
|
if name != "rangeValue":
|
||||||
raise UnsupportedProperty(name)
|
raise UnsupportedProperty(name)
|
||||||
|
|
||||||
|
# Fan Speed
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||||
speed = self.entity.attributes.get(fan.ATTR_SPEED)
|
speed = self.entity.attributes.get(fan.ATTR_SPEED)
|
||||||
return RANGE_FAN_MAP.get(speed, 0)
|
return RANGE_FAN_MAP.get(speed, 0)
|
||||||
|
|
||||||
|
# Cover Position
|
||||||
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
|
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION)
|
||||||
|
|
||||||
|
# Cover Tilt Position
|
||||||
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||||
|
return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def configuration(self):
|
def configuration(self):
|
||||||
"""Return configuration with presetResources."""
|
"""Return configuration with presetResources."""
|
||||||
return self.serialize_preset_resources()
|
if isinstance(self._resource, AlexaCapabilityResource):
|
||||||
|
return self._resource.serialize_configuration()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def capability_resources(self):
|
def capability_resources(self):
|
||||||
"""Return capabilityResources object."""
|
"""Return capabilityResources object."""
|
||||||
capability_resources = []
|
|
||||||
|
|
||||||
|
# Fan Speed Resources
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||||
return [{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_FANSPEED}]
|
self._resource = AlexaPresetResource(
|
||||||
|
labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED],
|
||||||
return capability_resources
|
min_value=1,
|
||||||
|
max_value=3,
|
||||||
def preset_resources(self):
|
precision=1,
|
||||||
"""Return presetResources object."""
|
|
||||||
preset_resources = []
|
|
||||||
|
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
|
||||||
preset_resources = {
|
|
||||||
"minimumValue": 1,
|
|
||||||
"maximumValue": 3,
|
|
||||||
"precision": 1,
|
|
||||||
"presets": [
|
|
||||||
{
|
|
||||||
"rangeValue": 1,
|
|
||||||
"names": [
|
|
||||||
{
|
|
||||||
"type": Catalog.LABEL_ASSET,
|
|
||||||
"value": Catalog.VALUE_MINIMUM,
|
|
||||||
},
|
|
||||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_LOW},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rangeValue": 2,
|
|
||||||
"names": [
|
|
||||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_MEDIUM}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rangeValue": 3,
|
|
||||||
"names": [
|
|
||||||
{
|
|
||||||
"type": Catalog.LABEL_ASSET,
|
|
||||||
"value": Catalog.VALUE_MAXIMUM,
|
|
||||||
},
|
|
||||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_HIGH},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
return preset_resources
|
|
||||||
|
|
||||||
def serialize_preset_resources(self):
|
|
||||||
"""Return PresetResources, friendlyNames serialized for an API response."""
|
|
||||||
preset_resources = []
|
|
||||||
resources = self.preset_resources()
|
|
||||||
for preset in resources["presets"]:
|
|
||||||
preset_resources.append(
|
|
||||||
{
|
|
||||||
"rangeValue": preset["rangeValue"],
|
|
||||||
"presetResources": {
|
|
||||||
"friendlyNames": self.serialize_friendly_names(preset["names"])
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
self._resource.add_preset(
|
||||||
|
value=1,
|
||||||
|
labels=[AlexaGlobalCatalog.VALUE_LOW, AlexaGlobalCatalog.VALUE_MINIMUM],
|
||||||
|
)
|
||||||
|
self._resource.add_preset(value=2, labels=[AlexaGlobalCatalog.VALUE_MEDIUM])
|
||||||
|
self._resource.add_preset(
|
||||||
|
value=3,
|
||||||
|
labels=[
|
||||||
|
AlexaGlobalCatalog.VALUE_HIGH,
|
||||||
|
AlexaGlobalCatalog.VALUE_MAXIMUM,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
return {
|
# Cover Position Resources
|
||||||
"supportedRange": {
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
"minimumValue": resources["minimumValue"],
|
self._resource = AlexaPresetResource(
|
||||||
"maximumValue": resources["maximumValue"],
|
["Position", AlexaGlobalCatalog.SETTING_OPENING],
|
||||||
"precision": resources["precision"],
|
min_value=0,
|
||||||
},
|
max_value=100,
|
||||||
"presets": preset_resources,
|
precision=1,
|
||||||
}
|
unit=AlexaGlobalCatalog.UNIT_PERCENT,
|
||||||
|
)
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
|
# Cover Tilt Position Resources
|
||||||
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||||
|
self._resource = AlexaPresetResource(
|
||||||
|
["Tilt Position", AlexaGlobalCatalog.SETTING_OPENING],
|
||||||
|
min_value=0,
|
||||||
|
max_value=100,
|
||||||
|
precision=1,
|
||||||
|
unit=AlexaGlobalCatalog.UNIT_PERCENT,
|
||||||
|
)
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def semantics(self):
|
||||||
|
"""Build and return semantics object."""
|
||||||
|
|
||||||
|
# Cover Position
|
||||||
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
|
self._semantics = AlexaSemantics()
|
||||||
|
self._semantics.add_action_to_directive(
|
||||||
|
[AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0}
|
||||||
|
)
|
||||||
|
self._semantics.add_action_to_directive(
|
||||||
|
[AlexaSemantics.ACTION_RAISE], "SetRangeValue", {"rangeValue": 100}
|
||||||
|
)
|
||||||
|
self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0)
|
||||||
|
self._semantics.add_states_to_range(
|
||||||
|
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
|
||||||
|
)
|
||||||
|
return self._semantics.serialize_semantics()
|
||||||
|
|
||||||
|
# Cover Tilt Position
|
||||||
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||||
|
self._semantics = AlexaSemantics()
|
||||||
|
self._semantics.add_action_to_directive(
|
||||||
|
[AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0}
|
||||||
|
)
|
||||||
|
self._semantics.add_action_to_directive(
|
||||||
|
[AlexaSemantics.ACTION_OPEN], "SetRangeValue", {"rangeValue": 100}
|
||||||
|
)
|
||||||
|
self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0)
|
||||||
|
self._semantics.add_states_to_range(
|
||||||
|
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
|
||||||
|
)
|
||||||
|
return self._semantics.serialize_semantics()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class AlexaToggleController(AlexaCapability):
|
class AlexaToggleController(AlexaCapability):
|
||||||
@ -1151,6 +1157,8 @@ class AlexaToggleController(AlexaCapability):
|
|||||||
def __init__(self, entity, instance, non_controllable=False):
|
def __init__(self, entity, instance, non_controllable=False):
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
super().__init__(entity, instance)
|
super().__init__(entity, instance)
|
||||||
|
self._resource = None
|
||||||
|
self._semantics = None
|
||||||
self.properties_non_controllable = lambda: non_controllable
|
self.properties_non_controllable = lambda: non_controllable
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -1174,6 +1182,7 @@ class AlexaToggleController(AlexaCapability):
|
|||||||
if name != "toggleState":
|
if name != "toggleState":
|
||||||
raise UnsupportedProperty(name)
|
raise UnsupportedProperty(name)
|
||||||
|
|
||||||
|
# Fan Oscillating
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||||
is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING))
|
is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING))
|
||||||
return "ON" if is_on else "OFF"
|
return "ON" if is_on else "OFF"
|
||||||
@ -1182,16 +1191,15 @@ class AlexaToggleController(AlexaCapability):
|
|||||||
|
|
||||||
def capability_resources(self):
|
def capability_resources(self):
|
||||||
"""Return capabilityResources object."""
|
"""Return capabilityResources object."""
|
||||||
capability_resources = []
|
|
||||||
|
|
||||||
|
# Fan Oscillating Resource
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||||
capability_resources = [
|
self._resource = AlexaCapabilityResource(
|
||||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_OSCILLATE},
|
[AlexaGlobalCatalog.SETTING_OSCILLATE, "Rotate", "Rotation"]
|
||||||
{"type": Catalog.LABEL_TEXT, "value": "Rotate"},
|
)
|
||||||
{"type": Catalog.LABEL_TEXT, "value": "Rotation"},
|
return self._resource.serialize_capability_resources()
|
||||||
]
|
|
||||||
|
|
||||||
return capability_resources
|
return None
|
||||||
|
|
||||||
|
|
||||||
class AlexaChannelController(AlexaCapability):
|
class AlexaChannelController(AlexaCapability):
|
||||||
|
@ -117,163 +117,6 @@ class Cause:
|
|||||||
VOICE_INTERACTION = "VOICE_INTERACTION"
|
VOICE_INTERACTION = "VOICE_INTERACTION"
|
||||||
|
|
||||||
|
|
||||||
class Catalog:
|
|
||||||
"""The Global Alexa catalog.
|
|
||||||
|
|
||||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog
|
|
||||||
|
|
||||||
You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units.
|
|
||||||
This catalog is localized into all the languages that Alexa supports.
|
|
||||||
|
|
||||||
You can reference the following catalog of pre-defined friendly names.
|
|
||||||
Each item in the following list is an asset identifier followed by its supported friendly names.
|
|
||||||
The first friendly name for each identifier is the one displayed in the Alexa mobile app.
|
|
||||||
"""
|
|
||||||
|
|
||||||
LABEL_ASSET = "asset"
|
|
||||||
LABEL_TEXT = "text"
|
|
||||||
|
|
||||||
# Shower
|
|
||||||
DEVICENAME_SHOWER = "Alexa.DeviceName.Shower"
|
|
||||||
|
|
||||||
# Washer, Washing Machine
|
|
||||||
DEVICENAME_WASHER = "Alexa.DeviceName.Washer"
|
|
||||||
|
|
||||||
# Router, Internet Router, Network Router, Wifi Router, Net Router
|
|
||||||
DEVICENAME_ROUTER = "Alexa.DeviceName.Router"
|
|
||||||
|
|
||||||
# Fan, Blower
|
|
||||||
DEVICENAME_FAN = "Alexa.DeviceName.Fan"
|
|
||||||
|
|
||||||
# Air Purifier, Air Cleaner,Clean Air Machine
|
|
||||||
DEVICENAME_AIRPURIFIER = "Alexa.DeviceName.AirPurifier"
|
|
||||||
|
|
||||||
# Space Heater, Portable Heater
|
|
||||||
DEVICENAME_SPACEHEATER = "Alexa.DeviceName.SpaceHeater"
|
|
||||||
|
|
||||||
# Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet
|
|
||||||
SHOWER_RAINHEAD = "Alexa.Shower.RainHead"
|
|
||||||
|
|
||||||
# Handheld Shower, Shower Wand, Hand Shower
|
|
||||||
SHOWER_HANDHELD = "Alexa.Shower.HandHeld"
|
|
||||||
|
|
||||||
# Water Temperature, Water Temp, Water Heat
|
|
||||||
SETTING_WATERTEMPERATURE = "Alexa.Setting.WaterTemperature"
|
|
||||||
|
|
||||||
# Temperature, Temp
|
|
||||||
SETTING_TEMPERATURE = "Alexa.Setting.Temperature"
|
|
||||||
|
|
||||||
# Wash Cycle, Wash Preset, Wash setting
|
|
||||||
SETTING_WASHCYCLE = "Alexa.Setting.WashCycle"
|
|
||||||
|
|
||||||
# 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi
|
|
||||||
SETTING_2GGUESTWIFI = "Alexa.Setting.2GGuestWiFi"
|
|
||||||
|
|
||||||
# 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi
|
|
||||||
SETTING_5GGUESTWIFI = "Alexa.Setting.5GGuestWiFi"
|
|
||||||
|
|
||||||
# Guest Wi-fi, Guest Network, Guest Net
|
|
||||||
SETTING_GUESTWIFI = "Alexa.Setting.GuestWiFi"
|
|
||||||
|
|
||||||
# Auto, Automatic, Automatic Mode, Auto Mode
|
|
||||||
SETTING_AUTO = "Alexa.Setting.Auto"
|
|
||||||
|
|
||||||
# #Night, Night Mode
|
|
||||||
SETTING_NIGHT = "Alexa.Setting.Night"
|
|
||||||
|
|
||||||
# Quiet, Quiet Mode, Noiseless, Silent
|
|
||||||
SETTING_QUIET = "Alexa.Setting.Quiet"
|
|
||||||
|
|
||||||
# Oscillate, Swivel, Oscillation, Spin, Back and forth
|
|
||||||
SETTING_OSCILLATE = "Alexa.Setting.Oscillate"
|
|
||||||
|
|
||||||
# Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity
|
|
||||||
SETTING_FANSPEED = "Alexa.Setting.FanSpeed"
|
|
||||||
|
|
||||||
# Preset, Setting
|
|
||||||
SETTING_PRESET = "Alexa.Setting.Preset"
|
|
||||||
|
|
||||||
# Mode
|
|
||||||
SETTING_MODE = "Alexa.Setting.Mode"
|
|
||||||
|
|
||||||
# Direction
|
|
||||||
SETTING_DIRECTION = "Alexa.Setting.Direction"
|
|
||||||
|
|
||||||
# Delicates, Delicate
|
|
||||||
VALUE_DELICATE = "Alexa.Value.Delicate"
|
|
||||||
|
|
||||||
# Quick Wash, Fast Wash, Wash Quickly, Speed Wash
|
|
||||||
VALUE_QUICKWASH = "Alexa.Value.QuickWash"
|
|
||||||
|
|
||||||
# Maximum, Max
|
|
||||||
VALUE_MAXIMUM = "Alexa.Value.Maximum"
|
|
||||||
|
|
||||||
# Minimum, Min
|
|
||||||
VALUE_MINIMUM = "Alexa.Value.Minimum"
|
|
||||||
|
|
||||||
# High
|
|
||||||
VALUE_HIGH = "Alexa.Value.High"
|
|
||||||
|
|
||||||
# Low
|
|
||||||
VALUE_LOW = "Alexa.Value.Low"
|
|
||||||
|
|
||||||
# Medium, Mid
|
|
||||||
VALUE_MEDIUM = "Alexa.Value.Medium"
|
|
||||||
|
|
||||||
|
|
||||||
class Unit:
|
|
||||||
"""Alexa Units of Measure.
|
|
||||||
|
|
||||||
https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#units-of-measure
|
|
||||||
"""
|
|
||||||
|
|
||||||
ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees"
|
|
||||||
|
|
||||||
ANGLE_RADIANS = "Alexa.Unit.Angle.Radians"
|
|
||||||
|
|
||||||
DISTANCE_FEET = "Alexa.Unit.Distance.Feet"
|
|
||||||
|
|
||||||
DISTANCE_INCHES = "Alexa.Unit.Distance.Inches"
|
|
||||||
|
|
||||||
DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers"
|
|
||||||
|
|
||||||
DISTANCE_METERS = "Alexa.Unit.Distance.Meters"
|
|
||||||
|
|
||||||
DISTANCE_MILES = "Alexa.Unit.Distance.Miles"
|
|
||||||
|
|
||||||
DISTANCE_YARDS = "Alexa.Unit.Distance.Yards"
|
|
||||||
|
|
||||||
MASS_GRAMS = "Alexa.Unit.Mass.Grams"
|
|
||||||
|
|
||||||
MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms"
|
|
||||||
|
|
||||||
PERCENT = "Alexa.Unit.Percent"
|
|
||||||
|
|
||||||
TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius"
|
|
||||||
|
|
||||||
TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees"
|
|
||||||
|
|
||||||
TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit"
|
|
||||||
|
|
||||||
TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin"
|
|
||||||
|
|
||||||
VOLUME_CUBICFEET = "Alexa.Unit.Volume.CubicFeet"
|
|
||||||
|
|
||||||
VOLUME_CUBICMETERS = "Alexa.Unit.Volume.CubicMeters"
|
|
||||||
|
|
||||||
VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons"
|
|
||||||
|
|
||||||
VOLUME_LITERS = "Alexa.Unit.Volume.Liters"
|
|
||||||
|
|
||||||
VOLUME_PINTS = "Alexa.Unit.Volume.Pints"
|
|
||||||
|
|
||||||
VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts"
|
|
||||||
|
|
||||||
WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces"
|
|
||||||
|
|
||||||
WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds"
|
|
||||||
|
|
||||||
|
|
||||||
class Inputs:
|
class Inputs:
|
||||||
"""Valid names for the InputController.
|
"""Valid names for the InputController.
|
||||||
|
|
||||||
|
@ -83,6 +83,9 @@ class DisplayCategory:
|
|||||||
# Indicates media devices with video or photo capabilities.
|
# Indicates media devices with video or photo capabilities.
|
||||||
CAMERA = "CAMERA"
|
CAMERA = "CAMERA"
|
||||||
|
|
||||||
|
# Indicates a non-mobile computer, such as a desktop computer.
|
||||||
|
COMPUTER = "COMPUTER"
|
||||||
|
|
||||||
# Indicates an endpoint that detects and reports contact.
|
# Indicates an endpoint that detects and reports contact.
|
||||||
CONTACT_SENSOR = "CONTACT_SENSOR"
|
CONTACT_SENSOR = "CONTACT_SENSOR"
|
||||||
|
|
||||||
@ -92,27 +95,60 @@ class DisplayCategory:
|
|||||||
# Indicates a doorbell.
|
# Indicates a doorbell.
|
||||||
DOORBELL = "DOORBELL"
|
DOORBELL = "DOORBELL"
|
||||||
|
|
||||||
|
# Indicates a window covering on the outside of a structure.
|
||||||
|
EXTERIOR_BLIND = "EXTERIOR_BLIND"
|
||||||
|
|
||||||
# Indicates a fan.
|
# Indicates a fan.
|
||||||
FAN = "FAN"
|
FAN = "FAN"
|
||||||
|
|
||||||
|
# Indicates a game console, such as Microsoft Xbox or Nintendo Switch
|
||||||
|
GAME_CONSOLE = "GAME_CONSOLE"
|
||||||
|
|
||||||
|
# Indicates a garage door. Garage doors must implement the ModeController interface to open and close the door.
|
||||||
|
GARAGE_DOOR = "GARAGE_DOOR"
|
||||||
|
|
||||||
|
# Indicates a window covering on the inside of a structure.
|
||||||
|
INTERIOR_BLIND = "INTERIOR_BLIND"
|
||||||
|
|
||||||
|
# Indicates a laptop or other mobile computer.
|
||||||
|
LAPTOP = "LAPTOP"
|
||||||
|
|
||||||
# Indicates light sources or fixtures.
|
# Indicates light sources or fixtures.
|
||||||
LIGHT = "LIGHT"
|
LIGHT = "LIGHT"
|
||||||
|
|
||||||
# Indicates a microwave oven.
|
# Indicates a microwave oven.
|
||||||
MICROWAVE = "MICROWAVE"
|
MICROWAVE = "MICROWAVE"
|
||||||
|
|
||||||
|
# Indicates a mobile phone.
|
||||||
|
MOBILE_PHONE = "MOBILE_PHONE"
|
||||||
|
|
||||||
# Indicates an endpoint that detects and reports motion.
|
# Indicates an endpoint that detects and reports motion.
|
||||||
MOTION_SENSOR = "MOTION_SENSOR"
|
MOTION_SENSOR = "MOTION_SENSOR"
|
||||||
|
|
||||||
|
# Indicates a network-connected music system.
|
||||||
|
MUSIC_SYSTEM = "MUSIC_SYSTEM"
|
||||||
|
|
||||||
# An endpoint that cannot be described in on of the other categories.
|
# An endpoint that cannot be described in on of the other categories.
|
||||||
OTHER = "OTHER"
|
OTHER = "OTHER"
|
||||||
|
|
||||||
|
# Indicates a network router.
|
||||||
|
NETWORK_HARDWARE = "NETWORK_HARDWARE"
|
||||||
|
|
||||||
|
# Indicates an oven cooking appliance.
|
||||||
|
OVEN = "OVEN"
|
||||||
|
|
||||||
|
# Indicates a non-mobile phone, such as landline or an IP phone.
|
||||||
|
PHONE = "PHONE"
|
||||||
|
|
||||||
# Describes a combination of devices set to a specific state, when the
|
# 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
|
# order of the state change is not important. For example a bedtime scene
|
||||||
# might include turning off lights and lowering the thermostat, but the
|
# might include turning off lights and lowering the thermostat, but the
|
||||||
# order is unimportant. Applies to Scenes
|
# order is unimportant. Applies to Scenes
|
||||||
SCENE_TRIGGER = "SCENE_TRIGGER"
|
SCENE_TRIGGER = "SCENE_TRIGGER"
|
||||||
|
|
||||||
|
# Indicates a projector screen.
|
||||||
|
SCREEN = "SCREEN"
|
||||||
|
|
||||||
# Indicates a security panel.
|
# Indicates a security panel.
|
||||||
SECURITY_PANEL = "SECURITY_PANEL"
|
SECURITY_PANEL = "SECURITY_PANEL"
|
||||||
|
|
||||||
@ -126,10 +162,16 @@ class DisplayCategory:
|
|||||||
# Indicates the endpoint is a speaker or speaker system.
|
# Indicates the endpoint is a speaker or speaker system.
|
||||||
SPEAKER = "SPEAKER"
|
SPEAKER = "SPEAKER"
|
||||||
|
|
||||||
|
# Indicates a streaming device such as Apple TV, Chromecast, or Roku.
|
||||||
|
STREAMING_DEVICE = "STREAMING_DEVICE"
|
||||||
|
|
||||||
# Indicates in-wall switches wired to the electrical system. Can control a
|
# Indicates in-wall switches wired to the electrical system. Can control a
|
||||||
# variety of devices.
|
# variety of devices.
|
||||||
SWITCH = "SWITCH"
|
SWITCH = "SWITCH"
|
||||||
|
|
||||||
|
# Indicates a tablet computer.
|
||||||
|
TABLET = "TABLET"
|
||||||
|
|
||||||
# Indicates endpoints that report the temperature only.
|
# Indicates endpoints that report the temperature only.
|
||||||
TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR"
|
TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR"
|
||||||
|
|
||||||
@ -140,6 +182,9 @@ class DisplayCategory:
|
|||||||
# Indicates the endpoint is a television.
|
# Indicates the endpoint is a television.
|
||||||
TV = "TV"
|
TV = "TV"
|
||||||
|
|
||||||
|
# Indicates a network-connected wearable device, such as an Apple Watch, Fitbit, or Samsung Gear.
|
||||||
|
WEARABLE = "WEARABLE"
|
||||||
|
|
||||||
|
|
||||||
class AlexaEntity:
|
class AlexaEntity:
|
||||||
"""An adaptation of an entity, expressed in Alexa's terms.
|
"""An adaptation of an entity, expressed in Alexa's terms.
|
||||||
@ -318,20 +363,40 @@ class CoverCapabilities(AlexaEntity):
|
|||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
"""Return the display categories for this entity."""
|
"""Return the display categories for this entity."""
|
||||||
device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS)
|
device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS)
|
||||||
if device_class in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_DOOR):
|
if device_class == cover.DEVICE_CLASS_GARAGE:
|
||||||
|
return [DisplayCategory.GARAGE_DOOR]
|
||||||
|
if device_class == cover.DEVICE_CLASS_DOOR:
|
||||||
return [DisplayCategory.DOOR]
|
return [DisplayCategory.DOOR]
|
||||||
|
if device_class in (
|
||||||
|
cover.DEVICE_CLASS_BLIND,
|
||||||
|
cover.DEVICE_CLASS_SHADE,
|
||||||
|
cover.DEVICE_CLASS_CURTAIN,
|
||||||
|
):
|
||||||
|
return [DisplayCategory.INTERIOR_BLIND]
|
||||||
|
if device_class in (
|
||||||
|
cover.DEVICE_CLASS_WINDOW,
|
||||||
|
cover.DEVICE_CLASS_AWNING,
|
||||||
|
cover.DEVICE_CLASS_SHUTTER,
|
||||||
|
):
|
||||||
|
return [DisplayCategory.EXTERIOR_BLIND]
|
||||||
|
|
||||||
return [DisplayCategory.OTHER]
|
return [DisplayCategory.OTHER]
|
||||||
|
|
||||||
def interfaces(self):
|
def interfaces(self):
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
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:
|
||||||
yield AlexaPercentageController(self.entity)
|
yield AlexaRangeController(
|
||||||
if supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN):
|
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
|
||||||
|
)
|
||||||
|
elif supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN):
|
||||||
yield AlexaModeController(
|
yield AlexaModeController(
|
||||||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
|
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
|
||||||
)
|
)
|
||||||
|
if supported & cover.SUPPORT_SET_TILT_POSITION:
|
||||||
|
yield AlexaRangeController(
|
||||||
|
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}"
|
||||||
|
)
|
||||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
yield Alexa(self.hass)
|
yield Alexa(self.hass)
|
||||||
|
|
||||||
@ -355,6 +420,7 @@ class LightCapabilities(AlexaEntity):
|
|||||||
yield AlexaColorController(self.entity)
|
yield AlexaColorController(self.entity)
|
||||||
if supported & light.SUPPORT_COLOR_TEMP:
|
if supported & light.SUPPORT_COLOR_TEMP:
|
||||||
yield AlexaColorTemperatureController(self.entity)
|
yield AlexaColorTemperatureController(self.entity)
|
||||||
|
|
||||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
yield Alexa(self.hass)
|
yield Alexa(self.hass)
|
||||||
|
|
||||||
@ -370,6 +436,7 @@ class FanCapabilities(AlexaEntity):
|
|||||||
def interfaces(self):
|
def interfaces(self):
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
yield AlexaPowerController(self.entity)
|
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:
|
||||||
yield AlexaPercentageController(self.entity)
|
yield AlexaPercentageController(self.entity)
|
||||||
@ -377,7 +444,6 @@ class FanCapabilities(AlexaEntity):
|
|||||||
yield AlexaRangeController(
|
yield AlexaRangeController(
|
||||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}"
|
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if supported & fan.SUPPORT_OSCILLATE:
|
if supported & fan.SUPPORT_OSCILLATE:
|
||||||
yield AlexaToggleController(
|
yield AlexaToggleController(
|
||||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
|
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
|
||||||
|
@ -20,6 +20,7 @@ from homeassistant.const import (
|
|||||||
SERVICE_MEDIA_PREVIOUS_TRACK,
|
SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||||
SERVICE_MEDIA_STOP,
|
SERVICE_MEDIA_STOP,
|
||||||
SERVICE_SET_COVER_POSITION,
|
SERVICE_SET_COVER_POSITION,
|
||||||
|
SERVICE_SET_COVER_TILT_POSITION,
|
||||||
SERVICE_TURN_OFF,
|
SERVICE_TURN_OFF,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
SERVICE_UNLOCK,
|
SERVICE_UNLOCK,
|
||||||
@ -28,8 +29,6 @@ from homeassistant.const import (
|
|||||||
SERVICE_VOLUME_SET,
|
SERVICE_VOLUME_SET,
|
||||||
SERVICE_VOLUME_UP,
|
SERVICE_VOLUME_UP,
|
||||||
STATE_ALARM_DISARMED,
|
STATE_ALARM_DISARMED,
|
||||||
STATE_CLOSED,
|
|
||||||
STATE_OPEN,
|
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
)
|
)
|
||||||
@ -113,9 +112,7 @@ async def async_api_turn_on(hass, config, directive, context):
|
|||||||
domain = ha.DOMAIN
|
domain = ha.DOMAIN
|
||||||
|
|
||||||
service = SERVICE_TURN_ON
|
service = SERVICE_TURN_ON
|
||||||
if domain == cover.DOMAIN:
|
if domain == media_player.DOMAIN:
|
||||||
service = cover.SERVICE_OPEN_COVER
|
|
||||||
elif domain == media_player.DOMAIN:
|
|
||||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
|
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
|
||||||
if not supported & power_features:
|
if not supported & power_features:
|
||||||
@ -141,9 +138,7 @@ async def async_api_turn_off(hass, config, directive, context):
|
|||||||
domain = ha.DOMAIN
|
domain = ha.DOMAIN
|
||||||
|
|
||||||
service = SERVICE_TURN_OFF
|
service = SERVICE_TURN_OFF
|
||||||
if entity.domain == cover.DOMAIN:
|
if domain == media_player.DOMAIN:
|
||||||
service = cover.SERVICE_CLOSE_COVER
|
|
||||||
elif domain == media_player.DOMAIN:
|
|
||||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
|
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
|
||||||
if not supported & power_features:
|
if not supported & power_features:
|
||||||
@ -348,10 +343,6 @@ async def async_api_set_percentage(hass, config, directive, context):
|
|||||||
speed = "high"
|
speed = "high"
|
||||||
data[fan.ATTR_SPEED] = speed
|
data[fan.ATTR_SPEED] = speed
|
||||||
|
|
||||||
elif entity.domain == cover.DOMAIN:
|
|
||||||
service = SERVICE_SET_COVER_POSITION
|
|
||||||
data[cover.ATTR_POSITION] = percentage
|
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
entity.domain, service, data, blocking=False, context=context
|
entity.domain, service, data, blocking=False, context=context
|
||||||
)
|
)
|
||||||
@ -385,13 +376,6 @@ async def async_api_adjust_percentage(hass, config, directive, context):
|
|||||||
|
|
||||||
data[fan.ATTR_SPEED] = speed
|
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)
|
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
entity.domain, service, data, blocking=False, context=context
|
entity.domain, service, data, blocking=False, context=context
|
||||||
)
|
)
|
||||||
@ -960,32 +944,35 @@ async def async_api_disarm(hass, config, directive, context):
|
|||||||
|
|
||||||
@HANDLERS.register(("Alexa.ModeController", "SetMode"))
|
@HANDLERS.register(("Alexa.ModeController", "SetMode"))
|
||||||
async def async_api_set_mode(hass, config, directive, context):
|
async def async_api_set_mode(hass, config, directive, context):
|
||||||
"""Process a next request."""
|
"""Process a SetMode directive."""
|
||||||
entity = directive.entity
|
entity = directive.entity
|
||||||
instance = directive.instance
|
instance = directive.instance
|
||||||
domain = entity.domain
|
domain = entity.domain
|
||||||
service = None
|
service = None
|
||||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
capability_mode = directive.payload["mode"]
|
mode = directive.payload["mode"]
|
||||||
|
|
||||||
if domain not in (fan.DOMAIN, cover.DOMAIN):
|
|
||||||
msg = "Entity does not support directive"
|
|
||||||
raise AlexaInvalidDirectiveError(msg)
|
|
||||||
|
|
||||||
|
# Fan Direction
|
||||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||||
_, direction = capability_mode.split(".")
|
_, direction = mode.split(".")
|
||||||
if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD):
|
if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD):
|
||||||
service = fan.SERVICE_SET_DIRECTION
|
service = fan.SERVICE_SET_DIRECTION
|
||||||
data[fan.ATTR_DIRECTION] = direction
|
data[fan.ATTR_DIRECTION] = direction
|
||||||
|
|
||||||
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
# Cover Position
|
||||||
_, position = capability_mode.split(".")
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
|
_, position = mode.split(".")
|
||||||
|
|
||||||
if position == STATE_CLOSED:
|
if position == cover.STATE_CLOSED:
|
||||||
service = cover.SERVICE_CLOSE_COVER
|
service = cover.SERVICE_CLOSE_COVER
|
||||||
|
elif position == cover.STATE_OPEN:
|
||||||
if position == STATE_OPEN:
|
|
||||||
service = cover.SERVICE_OPEN_COVER
|
service = cover.SERVICE_OPEN_COVER
|
||||||
|
elif position == "custom":
|
||||||
|
service = cover.SERVICE_STOP_COVER
|
||||||
|
|
||||||
|
else:
|
||||||
|
msg = "Entity does not support directive"
|
||||||
|
raise AlexaInvalidDirectiveError(msg)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
domain, service, data, blocking=False, context=context
|
domain, service, data, blocking=False, context=context
|
||||||
@ -997,7 +984,7 @@ async def async_api_set_mode(hass, config, directive, context):
|
|||||||
"namespace": "Alexa.ModeController",
|
"namespace": "Alexa.ModeController",
|
||||||
"instance": instance,
|
"instance": instance,
|
||||||
"name": "mode",
|
"name": "mode",
|
||||||
"value": capability_mode,
|
"value": mode,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1008,25 +995,14 @@ async def async_api_set_mode(hass, config, directive, context):
|
|||||||
async def async_api_adjust_mode(hass, config, directive, context):
|
async def async_api_adjust_mode(hass, config, directive, context):
|
||||||
"""Process a AdjustMode request.
|
"""Process a AdjustMode request.
|
||||||
|
|
||||||
Requires modeResources to be ordered.
|
Requires capabilityResources supportedModes to be ordered.
|
||||||
Only modes that are ordered support the adjustMode directive.
|
Only supportedModes with ordered=True support the adjustMode directive.
|
||||||
"""
|
"""
|
||||||
entity = directive.entity
|
|
||||||
instance = directive.instance
|
|
||||||
domain = entity.domain
|
|
||||||
|
|
||||||
if domain != fan.DOMAIN:
|
# Currently no supportedModes are configured with ordered=True to support this request.
|
||||||
msg = "Entity does not support directive"
|
msg = "Entity does not support directive"
|
||||||
raise AlexaInvalidDirectiveError(msg)
|
raise AlexaInvalidDirectiveError(msg)
|
||||||
|
|
||||||
if instance is None:
|
|
||||||
msg = "Entity does not support directive"
|
|
||||||
raise AlexaInvalidDirectiveError(msg)
|
|
||||||
|
|
||||||
# No modeResources are currently ordered to support this request.
|
|
||||||
|
|
||||||
return directive.response()
|
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register(("Alexa.ToggleController", "TurnOn"))
|
@HANDLERS.register(("Alexa.ToggleController", "TurnOn"))
|
||||||
async def async_api_toggle_on(hass, config, directive, context):
|
async def async_api_toggle_on(hass, config, directive, context):
|
||||||
@ -1037,19 +1013,29 @@ async def async_api_toggle_on(hass, config, directive, context):
|
|||||||
service = None
|
service = None
|
||||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
|
|
||||||
if domain != fan.DOMAIN:
|
# Fan Oscillating
|
||||||
msg = "Entity does not support directive"
|
|
||||||
raise AlexaInvalidDirectiveError(msg)
|
|
||||||
|
|
||||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||||
service = fan.SERVICE_OSCILLATE
|
service = fan.SERVICE_OSCILLATE
|
||||||
data[fan.ATTR_OSCILLATING] = True
|
data[fan.ATTR_OSCILLATING] = True
|
||||||
|
else:
|
||||||
|
msg = "Entity does not support directive"
|
||||||
|
raise AlexaInvalidDirectiveError(msg)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
domain, service, data, blocking=False, context=context
|
domain, service, data, blocking=False, context=context
|
||||||
)
|
)
|
||||||
|
|
||||||
return directive.response()
|
response = directive.response()
|
||||||
|
response.add_context_property(
|
||||||
|
{
|
||||||
|
"namespace": "Alexa.ToggleController",
|
||||||
|
"instance": instance,
|
||||||
|
"name": "toggleState",
|
||||||
|
"value": "ON",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register(("Alexa.ToggleController", "TurnOff"))
|
@HANDLERS.register(("Alexa.ToggleController", "TurnOff"))
|
||||||
@ -1061,19 +1047,29 @@ async def async_api_toggle_off(hass, config, directive, context):
|
|||||||
service = None
|
service = None
|
||||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
|
|
||||||
if domain != fan.DOMAIN:
|
# Fan Oscillating
|
||||||
msg = "Entity does not support directive"
|
|
||||||
raise AlexaInvalidDirectiveError(msg)
|
|
||||||
|
|
||||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||||
service = fan.SERVICE_OSCILLATE
|
service = fan.SERVICE_OSCILLATE
|
||||||
data[fan.ATTR_OSCILLATING] = False
|
data[fan.ATTR_OSCILLATING] = False
|
||||||
|
else:
|
||||||
|
msg = "Entity does not support directive"
|
||||||
|
raise AlexaInvalidDirectiveError(msg)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
domain, service, data, blocking=False, context=context
|
domain, service, data, blocking=False, context=context
|
||||||
)
|
)
|
||||||
|
|
||||||
return directive.response()
|
response = directive.response()
|
||||||
|
response.add_context_property(
|
||||||
|
{
|
||||||
|
"namespace": "Alexa.ToggleController",
|
||||||
|
"instance": instance,
|
||||||
|
"name": "toggleState",
|
||||||
|
"value": "OFF",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register(("Alexa.RangeController", "SetRangeValue"))
|
@HANDLERS.register(("Alexa.RangeController", "SetRangeValue"))
|
||||||
@ -1086,10 +1082,7 @@ async def async_api_set_range(hass, config, directive, context):
|
|||||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
range_value = int(directive.payload["rangeValue"])
|
range_value = int(directive.payload["rangeValue"])
|
||||||
|
|
||||||
if domain != fan.DOMAIN:
|
# Fan Speed
|
||||||
msg = "Entity does not support directive"
|
|
||||||
raise AlexaInvalidDirectiveError(msg)
|
|
||||||
|
|
||||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||||
service = fan.SERVICE_SET_SPEED
|
service = fan.SERVICE_SET_SPEED
|
||||||
speed = SPEED_FAN_MAP.get(range_value, None)
|
speed = SPEED_FAN_MAP.get(range_value, None)
|
||||||
@ -1103,11 +1096,45 @@ async def async_api_set_range(hass, config, directive, context):
|
|||||||
|
|
||||||
data[fan.ATTR_SPEED] = speed
|
data[fan.ATTR_SPEED] = speed
|
||||||
|
|
||||||
|
# Cover Position
|
||||||
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
|
if range_value == 0:
|
||||||
|
service = cover.SERVICE_CLOSE_COVER
|
||||||
|
elif range_value == 100:
|
||||||
|
service = cover.SERVICE_OPEN_COVER
|
||||||
|
else:
|
||||||
|
service = cover.SERVICE_SET_COVER_POSITION
|
||||||
|
data[cover.ATTR_POSITION] = range_value
|
||||||
|
|
||||||
|
# Cover Tilt Position
|
||||||
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||||
|
if range_value == 0:
|
||||||
|
service = cover.SERVICE_CLOSE_COVER_TILT
|
||||||
|
elif range_value == 100:
|
||||||
|
service = cover.SERVICE_OPEN_COVER_TILT
|
||||||
|
else:
|
||||||
|
service = cover.SERVICE_SET_COVER_TILT_POSITION
|
||||||
|
data[cover.ATTR_POSITION] = range_value
|
||||||
|
|
||||||
|
else:
|
||||||
|
msg = "Entity does not support directive"
|
||||||
|
raise AlexaInvalidDirectiveError(msg)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
domain, service, data, blocking=False, context=context
|
domain, service, data, blocking=False, context=context
|
||||||
)
|
)
|
||||||
|
|
||||||
return directive.response()
|
response = directive.response()
|
||||||
|
response.add_context_property(
|
||||||
|
{
|
||||||
|
"namespace": "Alexa.RangeController",
|
||||||
|
"instance": instance,
|
||||||
|
"name": "rangeValue",
|
||||||
|
"value": range_value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue"))
|
@HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue"))
|
||||||
@ -1119,24 +1146,56 @@ async def async_api_adjust_range(hass, config, directive, context):
|
|||||||
service = None
|
service = None
|
||||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
range_delta = int(directive.payload["rangeValueDelta"])
|
range_delta = int(directive.payload["rangeValueDelta"])
|
||||||
|
response_value = 0
|
||||||
|
|
||||||
|
# Fan Speed
|
||||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||||
service = fan.SERVICE_SET_SPEED
|
service = fan.SERVICE_SET_SPEED
|
||||||
|
|
||||||
# adjust range
|
|
||||||
current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0)
|
current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0)
|
||||||
speed = SPEED_FAN_MAP.get(max(0, range_delta + current_range), fan.SPEED_OFF)
|
speed = SPEED_FAN_MAP.get(
|
||||||
|
min(3, max(0, range_delta + current_range)), fan.SPEED_OFF
|
||||||
|
)
|
||||||
|
|
||||||
if speed == fan.SPEED_OFF:
|
if speed == fan.SPEED_OFF:
|
||||||
service = fan.SERVICE_TURN_OFF
|
service = fan.SERVICE_TURN_OFF
|
||||||
|
|
||||||
data[fan.ATTR_SPEED] = speed
|
data[fan.ATTR_SPEED] = response_value = speed
|
||||||
|
|
||||||
|
# Cover Position
|
||||||
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
|
service = SERVICE_SET_COVER_POSITION
|
||||||
|
current = entity.attributes.get(cover.ATTR_POSITION)
|
||||||
|
data[cover.ATTR_POSITION] = response_value = min(
|
||||||
|
100, max(0, range_delta + current)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cover Tilt Position
|
||||||
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||||
|
service = SERVICE_SET_COVER_TILT_POSITION
|
||||||
|
current = entity.attributes.get(cover.ATTR_TILT_POSITION)
|
||||||
|
data[cover.ATTR_TILT_POSITION] = response_value = min(
|
||||||
|
100, max(0, range_delta + current)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
msg = "Entity does not support directive"
|
||||||
|
raise AlexaInvalidDirectiveError(msg)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
domain, service, data, blocking=False, context=context
|
domain, service, data, blocking=False, context=context
|
||||||
)
|
)
|
||||||
|
|
||||||
return directive.response()
|
response = directive.response()
|
||||||
|
response.add_context_property(
|
||||||
|
{
|
||||||
|
"namespace": "Alexa.RangeController",
|
||||||
|
"instance": instance,
|
||||||
|
"name": "rangeValue",
|
||||||
|
"value": response_value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register(("Alexa.ChannelController", "ChangeChannel"))
|
@HANDLERS.register(("Alexa.ChannelController", "ChangeChannel"))
|
||||||
|
387
homeassistant/components/alexa/resources.py
Normal file
387
homeassistant/components/alexa/resources.py
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
"""Alexa Resources and Assets."""
|
||||||
|
|
||||||
|
|
||||||
|
class AlexaGlobalCatalog:
|
||||||
|
"""The Global Alexa catalog.
|
||||||
|
|
||||||
|
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog
|
||||||
|
|
||||||
|
You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units.
|
||||||
|
This catalog is localized into all the languages that Alexa supports.
|
||||||
|
|
||||||
|
You can reference the following catalog of pre-defined friendly names.
|
||||||
|
Each item in the following list is an asset identifier followed by its supported friendly names.
|
||||||
|
The first friendly name for each identifier is the one displayed in the Alexa mobile app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Air Purifier, Air Cleaner,Clean Air Machine
|
||||||
|
DEVICE_NAME_AIR_PURIFIER = "Alexa.DeviceName.AirPurifier"
|
||||||
|
|
||||||
|
# Fan, Blower
|
||||||
|
DEVICE_NAME_FAN = "Alexa.DeviceName.Fan"
|
||||||
|
|
||||||
|
# Router, Internet Router, Network Router, Wifi Router, Net Router
|
||||||
|
DEVICE_NAME_ROUTER = "Alexa.DeviceName.Router"
|
||||||
|
|
||||||
|
# Shade, Blind, Curtain, Roller, Shutter, Drape, Awning, Window shade, Interior blind
|
||||||
|
DEVICE_NAME_SHADE = "Alexa.DeviceName.Shade"
|
||||||
|
|
||||||
|
# Shower
|
||||||
|
DEVICE_NAME_SHOWER = "Alexa.DeviceName.Shower"
|
||||||
|
|
||||||
|
# Space Heater, Portable Heater
|
||||||
|
DEVICE_NAME_SPACE_HEATER = "Alexa.DeviceName.SpaceHeater"
|
||||||
|
|
||||||
|
# Washer, Washing Machine
|
||||||
|
DEVICE_NAME_WASHER = "Alexa.DeviceName.Washer"
|
||||||
|
|
||||||
|
# 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi
|
||||||
|
SETTING_2G_GUEST_WIFI = "Alexa.Setting.2GGuestWiFi"
|
||||||
|
|
||||||
|
# 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi
|
||||||
|
SETTING_5G_GUEST_WIFI = "Alexa.Setting.5GGuestWiFi"
|
||||||
|
|
||||||
|
# Auto, Automatic, Automatic Mode, Auto Mode
|
||||||
|
SETTING_AUTO = "Alexa.Setting.Auto"
|
||||||
|
|
||||||
|
# Direction
|
||||||
|
SETTING_DIRECTION = "Alexa.Setting.Direction"
|
||||||
|
|
||||||
|
# Dry Cycle, Dry Preset, Dry Setting, Dryer Cycle, Dryer Preset, Dryer Setting
|
||||||
|
SETTING_DRY_CYCLE = "Alexa.Setting.DryCycle"
|
||||||
|
|
||||||
|
# Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity
|
||||||
|
SETTING_FAN_SPEED = "Alexa.Setting.FanSpeed"
|
||||||
|
|
||||||
|
# Guest Wi-fi, Guest Network, Guest Net
|
||||||
|
SETTING_GUEST_WIFI = "Alexa.Setting.GuestWiFi"
|
||||||
|
|
||||||
|
# Heat
|
||||||
|
SETTING_HEAT = "Alexa.Setting.Heat"
|
||||||
|
|
||||||
|
# Mode
|
||||||
|
SETTING_MODE = "Alexa.Setting.Mode"
|
||||||
|
|
||||||
|
# Night, Night Mode
|
||||||
|
SETTING_NIGHT = "Alexa.Setting.Night"
|
||||||
|
|
||||||
|
# Opening, Height, Lift, Width
|
||||||
|
SETTING_OPENING = "Alexa.Setting.Opening"
|
||||||
|
|
||||||
|
# Oscillate, Swivel, Oscillation, Spin, Back and forth
|
||||||
|
SETTING_OSCILLATE = "Alexa.Setting.Oscillate"
|
||||||
|
|
||||||
|
# Preset, Setting
|
||||||
|
SETTING_PRESET = "Alexa.Setting.Preset"
|
||||||
|
|
||||||
|
# Quiet, Quiet Mode, Noiseless, Silent
|
||||||
|
SETTING_QUIET = "Alexa.Setting.Quiet"
|
||||||
|
|
||||||
|
# Temperature, Temp
|
||||||
|
SETTING_TEMPERATURE = "Alexa.Setting.Temperature"
|
||||||
|
|
||||||
|
# Wash Cycle, Wash Preset, Wash setting
|
||||||
|
SETTING_WASH_CYCLE = "Alexa.Setting.WashCycle"
|
||||||
|
|
||||||
|
# Water Temperature, Water Temp, Water Heat
|
||||||
|
SETTING_WATER_TEMPERATURE = "Alexa.Setting.WaterTemperature"
|
||||||
|
|
||||||
|
# Handheld Shower, Shower Wand, Hand Shower
|
||||||
|
SHOWER_HAND_HELD = "Alexa.Shower.HandHeld"
|
||||||
|
|
||||||
|
# Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet
|
||||||
|
SHOWER_RAIN_HEAD = "Alexa.Shower.RainHead"
|
||||||
|
|
||||||
|
# Degrees, Degree
|
||||||
|
UNIT_ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees"
|
||||||
|
|
||||||
|
# Radians, Radian
|
||||||
|
UNIT_ANGLE_RADIANS = "Alexa.Unit.Angle.Radians"
|
||||||
|
|
||||||
|
# Feet, Foot
|
||||||
|
UNIT_DISTANCE_FEET = "Alexa.Unit.Distance.Feet"
|
||||||
|
|
||||||
|
# Inches, Inch
|
||||||
|
UNIT_DISTANCE_INCHES = "Alexa.Unit.Distance.Inches"
|
||||||
|
|
||||||
|
# Kilometers
|
||||||
|
UNIT_DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers"
|
||||||
|
|
||||||
|
# Meters, Meter, m
|
||||||
|
UNIT_DISTANCE_METERS = "Alexa.Unit.Distance.Meters"
|
||||||
|
|
||||||
|
# Miles, Mile
|
||||||
|
UNIT_DISTANCE_MILES = "Alexa.Unit.Distance.Miles"
|
||||||
|
|
||||||
|
# Yards, Yard
|
||||||
|
UNIT_DISTANCE_YARDS = "Alexa.Unit.Distance.Yards"
|
||||||
|
|
||||||
|
# Grams, Gram, g
|
||||||
|
UNIT_MASS_GRAMS = "Alexa.Unit.Mass.Grams"
|
||||||
|
|
||||||
|
# Kilograms, Kilogram, kg
|
||||||
|
UNIT_MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms"
|
||||||
|
|
||||||
|
# Percent
|
||||||
|
UNIT_PERCENT = "Alexa.Unit.Percent"
|
||||||
|
|
||||||
|
# Celsius, Degrees Celsius, Degrees, C, Centigrade, Degrees Centigrade
|
||||||
|
UNIT_TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius"
|
||||||
|
|
||||||
|
# Degrees, Degree
|
||||||
|
UNIT_TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees"
|
||||||
|
|
||||||
|
# Fahrenheit, Degrees Fahrenheit, Degrees F, Degrees, F
|
||||||
|
UNIT_TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit"
|
||||||
|
|
||||||
|
# Kelvin, Degrees Kelvin, Degrees K, Degrees, K
|
||||||
|
UNIT_TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin"
|
||||||
|
|
||||||
|
# Cubic Feet, Cubic Foot
|
||||||
|
UNIT_VOLUME_CUBIC_FEET = "Alexa.Unit.Volume.CubicFeet"
|
||||||
|
|
||||||
|
# Cubic Meters, Cubic Meter, Meters Cubed
|
||||||
|
UNIT_VOLUME_CUBIC_METERS = "Alexa.Unit.Volume.CubicMeters"
|
||||||
|
|
||||||
|
# Gallons, Gallon
|
||||||
|
UNIT_VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons"
|
||||||
|
|
||||||
|
# Liters, Liter, L
|
||||||
|
UNIT_VOLUME_LITERS = "Alexa.Unit.Volume.Liters"
|
||||||
|
|
||||||
|
# Pints, Pint
|
||||||
|
UNIT_VOLUME_PINTS = "Alexa.Unit.Volume.Pints"
|
||||||
|
|
||||||
|
# Quarts, Quart
|
||||||
|
UNIT_VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts"
|
||||||
|
|
||||||
|
# Ounces, Ounce, oz
|
||||||
|
UNIT_WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces"
|
||||||
|
|
||||||
|
# Pounds, Pound, lbs
|
||||||
|
UNIT_WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds"
|
||||||
|
|
||||||
|
# Close
|
||||||
|
VALUE_CLOSE = "Alexa.Value.Close"
|
||||||
|
|
||||||
|
# Delicates, Delicate
|
||||||
|
VALUE_DELICATE = "Alexa.Value.Delicate"
|
||||||
|
|
||||||
|
# High
|
||||||
|
VALUE_HIGH = "Alexa.Value.High"
|
||||||
|
|
||||||
|
# Low
|
||||||
|
VALUE_LOW = "Alexa.Value.Low"
|
||||||
|
|
||||||
|
# Maximum, Max
|
||||||
|
VALUE_MAXIMUM = "Alexa.Value.Maximum"
|
||||||
|
|
||||||
|
# Medium, Mid
|
||||||
|
VALUE_MEDIUM = "Alexa.Value.Medium"
|
||||||
|
|
||||||
|
# Minimum, Min
|
||||||
|
VALUE_MINIMUM = "Alexa.Value.Minimum"
|
||||||
|
|
||||||
|
# Open
|
||||||
|
VALUE_OPEN = "Alexa.Value.Open"
|
||||||
|
|
||||||
|
# Quick Wash, Fast Wash, Wash Quickly, Speed Wash
|
||||||
|
VALUE_QUICK_WASH = "Alexa.Value.QuickWash"
|
||||||
|
|
||||||
|
|
||||||
|
class AlexaCapabilityResource:
|
||||||
|
"""Base class for Alexa capabilityResources, ModeResources, and presetResources objects.
|
||||||
|
|
||||||
|
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, labels):
|
||||||
|
"""Initialize an Alexa resource."""
|
||||||
|
self._resource_labels = []
|
||||||
|
for label in labels:
|
||||||
|
self._resource_labels.append(label)
|
||||||
|
|
||||||
|
def serialize_capability_resources(self):
|
||||||
|
"""Return capabilityResources object serialized for an API response."""
|
||||||
|
return self.serialize_labels(self._resource_labels)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def serialize_configuration():
|
||||||
|
"""Return ModeResources, PresetResources friendlyNames serialized for an API response."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def serialize_labels(resources):
|
||||||
|
"""Return resource label objects for friendlyNames serialized for an API response."""
|
||||||
|
labels = []
|
||||||
|
for label in resources:
|
||||||
|
if label in AlexaGlobalCatalog.__dict__.values():
|
||||||
|
label = {"@type": "asset", "value": {"assetId": label}}
|
||||||
|
else:
|
||||||
|
label = {"@type": "text", "value": {"text": label, "locale": "en-US"}}
|
||||||
|
|
||||||
|
labels.append(label)
|
||||||
|
|
||||||
|
return {"friendlyNames": labels}
|
||||||
|
|
||||||
|
|
||||||
|
class AlexaModeResource(AlexaCapabilityResource):
|
||||||
|
"""Implements Alexa ModeResources.
|
||||||
|
|
||||||
|
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, labels, ordered=False):
|
||||||
|
"""Initialize an Alexa modeResource."""
|
||||||
|
super().__init__(labels)
|
||||||
|
self._supported_modes = []
|
||||||
|
self._mode_ordered = ordered
|
||||||
|
|
||||||
|
def add_mode(self, value, labels):
|
||||||
|
"""Add mode to the supportedModes object."""
|
||||||
|
self._supported_modes.append({"value": value, "labels": labels})
|
||||||
|
|
||||||
|
def serialize_configuration(self):
|
||||||
|
"""Return configuration for ModeResources friendlyNames serialized for an API response."""
|
||||||
|
mode_resources = []
|
||||||
|
for mode in self._supported_modes:
|
||||||
|
result = {
|
||||||
|
"value": mode["value"],
|
||||||
|
"modeResources": self.serialize_labels(mode["labels"]),
|
||||||
|
}
|
||||||
|
mode_resources.append(result)
|
||||||
|
|
||||||
|
return {"ordered": self._mode_ordered, "supportedModes": mode_resources}
|
||||||
|
|
||||||
|
|
||||||
|
class AlexaPresetResource(AlexaCapabilityResource):
|
||||||
|
"""Implements Alexa PresetResources.
|
||||||
|
|
||||||
|
Use presetResources with RangeController to provide a set of friendlyNames for each RangeController preset.
|
||||||
|
|
||||||
|
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, labels, min_value, max_value, precision, unit=None):
|
||||||
|
"""Initialize an Alexa presetResource."""
|
||||||
|
super().__init__(labels)
|
||||||
|
self._presets = []
|
||||||
|
self._minimum_value = int(min_value)
|
||||||
|
self._maximum_value = int(max_value)
|
||||||
|
self._precision = int(precision)
|
||||||
|
self._unit_of_measure = None
|
||||||
|
if unit in AlexaGlobalCatalog.__dict__.values():
|
||||||
|
self._unit_of_measure = unit
|
||||||
|
|
||||||
|
def add_preset(self, value, labels):
|
||||||
|
"""Add preset to configuration presets array."""
|
||||||
|
self._presets.append({"value": value, "labels": labels})
|
||||||
|
|
||||||
|
def serialize_configuration(self):
|
||||||
|
"""Return configuration for PresetResources friendlyNames serialized for an API response."""
|
||||||
|
configuration = {
|
||||||
|
"supportedRange": {
|
||||||
|
"minimumValue": self._minimum_value,
|
||||||
|
"maximumValue": self._maximum_value,
|
||||||
|
"precision": self._precision,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self._unit_of_measure:
|
||||||
|
configuration["unitOfMeasure"] = self._unit_of_measure
|
||||||
|
|
||||||
|
if self._presets:
|
||||||
|
preset_resources = []
|
||||||
|
for preset in self._presets:
|
||||||
|
preset_resources.append(
|
||||||
|
{
|
||||||
|
"rangeValue": preset["value"],
|
||||||
|
"presetResources": self.serialize_labels(preset["labels"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
configuration["presets"] = preset_resources
|
||||||
|
|
||||||
|
return configuration
|
||||||
|
|
||||||
|
|
||||||
|
class AlexaSemantics:
|
||||||
|
"""Class for Alexa Semantics Object.
|
||||||
|
|
||||||
|
You can optionally enable additional utterances by using semantics. When you use semantics,
|
||||||
|
you manually map the phrases "open", "close", "raise", and "lower" to directives.
|
||||||
|
|
||||||
|
Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController.
|
||||||
|
|
||||||
|
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object
|
||||||
|
"""
|
||||||
|
|
||||||
|
MAPPINGS_ACTION = "actionMappings"
|
||||||
|
MAPPINGS_STATE = "stateMappings"
|
||||||
|
|
||||||
|
ACTIONS_TO_DIRECTIVE = "ActionsToDirective"
|
||||||
|
STATES_TO_VALUE = "StatesToValue"
|
||||||
|
STATES_TO_RANGE = "StatesToRange"
|
||||||
|
|
||||||
|
ACTION_CLOSE = "Alexa.Actions.Close"
|
||||||
|
ACTION_LOWER = "Alexa.Actions.Lower"
|
||||||
|
ACTION_OPEN = "Alexa.Actions.Open"
|
||||||
|
ACTION_RAISE = "Alexa.Actions.Raise"
|
||||||
|
|
||||||
|
STATES_OPEN = "Alexa.States.Open"
|
||||||
|
STATES_CLOSED = "Alexa.States.Closed"
|
||||||
|
|
||||||
|
DIRECTIVE_RANGE_SET_VALUE = "SetRangeValue"
|
||||||
|
DIRECTIVE_RANGE_ADJUST_VALUE = "AdjustRangeValue"
|
||||||
|
DIRECTIVE_TOGGLE_TURN_ON = "TurnOn"
|
||||||
|
DIRECTIVE_TOGGLE_TURN_OFF = "TurnOff"
|
||||||
|
DIRECTIVE_MODE_SET_MODE = "SetMode"
|
||||||
|
DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize an Alexa modeResource."""
|
||||||
|
self._action_mappings = []
|
||||||
|
self._state_mappings = []
|
||||||
|
|
||||||
|
def _add_action_mapping(self, semantics):
|
||||||
|
"""Add action mapping between actions and interface directives."""
|
||||||
|
self._action_mappings.append(semantics)
|
||||||
|
|
||||||
|
def _add_state_mapping(self, semantics):
|
||||||
|
"""Add state mapping between states and interface directives."""
|
||||||
|
self._state_mappings.append(semantics)
|
||||||
|
|
||||||
|
def add_states_to_value(self, states, value):
|
||||||
|
"""Add StatesToValue stateMappings."""
|
||||||
|
self._add_state_mapping(
|
||||||
|
{"@type": self.STATES_TO_VALUE, "states": states, "value": value}
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_states_to_range(self, states, min_value, max_value):
|
||||||
|
"""Add StatesToRange stateMappings."""
|
||||||
|
self._add_state_mapping(
|
||||||
|
{
|
||||||
|
"@type": self.STATES_TO_RANGE,
|
||||||
|
"states": states,
|
||||||
|
"range": {"minimumValue": min_value, "maximumValue": max_value},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_action_to_directive(self, actions, directive, payload):
|
||||||
|
"""Add ActionsToDirective actionMappings."""
|
||||||
|
self._add_action_mapping(
|
||||||
|
{
|
||||||
|
"@type": self.ACTIONS_TO_DIRECTIVE,
|
||||||
|
"actions": actions,
|
||||||
|
"directive": {"name": directive, "payload": payload},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def serialize_semantics(self):
|
||||||
|
"""Return semantics object serialized for an API response."""
|
||||||
|
semantics = {}
|
||||||
|
if self._action_mappings:
|
||||||
|
semantics[self.MAPPINGS_ACTION] = self._action_mappings
|
||||||
|
if self._state_mappings:
|
||||||
|
semantics[self.MAPPINGS_STATE] = self._state_mappings
|
||||||
|
|
||||||
|
return semantics
|
@ -411,14 +411,14 @@ async def test_report_fan_direction(hass):
|
|||||||
properties.assert_not_has_property("Alexa.ModeController", "mode")
|
properties.assert_not_has_property("Alexa.ModeController", "mode")
|
||||||
|
|
||||||
properties = await reported_properties(hass, "fan.reverse")
|
properties = await reported_properties(hass, "fan.reverse")
|
||||||
properties.assert_equal("Alexa.ModeController", "mode", "reverse")
|
properties.assert_equal("Alexa.ModeController", "mode", "direction.reverse")
|
||||||
|
|
||||||
properties = await reported_properties(hass, "fan.forward")
|
properties = await reported_properties(hass, "fan.forward")
|
||||||
properties.assert_equal("Alexa.ModeController", "mode", "forward")
|
properties.assert_equal("Alexa.ModeController", "mode", "direction.forward")
|
||||||
|
|
||||||
|
|
||||||
async def test_report_cover_percentage_state(hass):
|
async def test_report_cover_range_value(hass):
|
||||||
"""Test PercentageController reports cover percentage correctly."""
|
"""Test RangeController reports cover position correctly."""
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"cover.fully_open",
|
"cover.fully_open",
|
||||||
"open",
|
"open",
|
||||||
@ -448,13 +448,13 @@ async def test_report_cover_percentage_state(hass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
properties = await reported_properties(hass, "cover.fully_open")
|
properties = await reported_properties(hass, "cover.fully_open")
|
||||||
properties.assert_equal("Alexa.PercentageController", "percentage", 100)
|
properties.assert_equal("Alexa.RangeController", "rangeValue", 100)
|
||||||
|
|
||||||
properties = await reported_properties(hass, "cover.half_open")
|
properties = await reported_properties(hass, "cover.half_open")
|
||||||
properties.assert_equal("Alexa.PercentageController", "percentage", 50)
|
properties.assert_equal("Alexa.RangeController", "rangeValue", 50)
|
||||||
|
|
||||||
properties = await reported_properties(hass, "cover.closed")
|
properties = await reported_properties(hass, "cover.closed")
|
||||||
properties.assert_equal("Alexa.PercentageController", "percentage", 0)
|
properties.assert_equal("Alexa.RangeController", "rangeValue", 0)
|
||||||
|
|
||||||
|
|
||||||
async def test_report_climate_state(hass):
|
async def test_report_climate_state(hass):
|
||||||
|
@ -126,10 +126,12 @@ async def discovery_test(device, hass, expected_endpoints=1):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_capability(capabilities, capability_name):
|
def get_capability(capabilities, capability_name, instance=None):
|
||||||
"""Search a set of capabilities for a specific one."""
|
"""Search a set of capabilities for a specific one."""
|
||||||
for capability in capabilities:
|
for capability in capabilities:
|
||||||
if capability["interface"] == capability_name:
|
if instance and capability["instance"] == instance:
|
||||||
|
return capability
|
||||||
|
elif capability["interface"] == capability_name:
|
||||||
return capability
|
return capability
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@ -420,9 +422,29 @@ async def test_variable_fan(hass):
|
|||||||
)
|
)
|
||||||
assert call.data["speed"] == "medium"
|
assert call.data["speed"] == "medium"
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
"Alexa.PercentageController",
|
||||||
|
"SetPercentage",
|
||||||
|
"fan#test_2",
|
||||||
|
"fan.set_speed",
|
||||||
|
hass,
|
||||||
|
payload={"percentage": "33"},
|
||||||
|
)
|
||||||
|
assert call.data["speed"] == "low"
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
"Alexa.PercentageController",
|
||||||
|
"SetPercentage",
|
||||||
|
"fan#test_2",
|
||||||
|
"fan.set_speed",
|
||||||
|
hass,
|
||||||
|
payload={"percentage": "100"},
|
||||||
|
)
|
||||||
|
assert call.data["speed"] == "high"
|
||||||
|
|
||||||
await assert_percentage_changes(
|
await assert_percentage_changes(
|
||||||
hass,
|
hass,
|
||||||
[("high", "-5"), ("off", "5"), ("low", "-80")],
|
[("high", "-5"), ("off", "5"), ("low", "-80"), ("medium", "-34")],
|
||||||
"Alexa.PercentageController",
|
"Alexa.PercentageController",
|
||||||
"AdjustPercentage",
|
"AdjustPercentage",
|
||||||
"fan#test_2",
|
"fan#test_2",
|
||||||
@ -431,6 +453,16 @@ async def test_variable_fan(hass):
|
|||||||
"speed",
|
"speed",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
"Alexa.PowerLevelController",
|
||||||
|
"SetPowerLevel",
|
||||||
|
"fan#test_2",
|
||||||
|
"fan.set_speed",
|
||||||
|
hass,
|
||||||
|
payload={"powerLevel": "20"},
|
||||||
|
)
|
||||||
|
assert call.data["speed"] == "low"
|
||||||
|
|
||||||
call, _ = await assert_request_calls_service(
|
call, _ = await assert_request_calls_service(
|
||||||
"Alexa.PowerLevelController",
|
"Alexa.PowerLevelController",
|
||||||
"SetPowerLevel",
|
"SetPowerLevel",
|
||||||
@ -441,6 +473,16 @@ async def test_variable_fan(hass):
|
|||||||
)
|
)
|
||||||
assert call.data["speed"] == "medium"
|
assert call.data["speed"] == "medium"
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
"Alexa.PowerLevelController",
|
||||||
|
"SetPowerLevel",
|
||||||
|
"fan#test_2",
|
||||||
|
"fan.set_speed",
|
||||||
|
hass,
|
||||||
|
payload={"powerLevel": "99"},
|
||||||
|
)
|
||||||
|
assert call.data["speed"] == "high"
|
||||||
|
|
||||||
await assert_percentage_changes(
|
await assert_percentage_changes(
|
||||||
hass,
|
hass,
|
||||||
[("high", "-5"), ("medium", "-50"), ("low", "-80")],
|
[("high", "-5"), ("medium", "-50"), ("low", "-80")],
|
||||||
@ -1333,51 +1375,106 @@ async def test_group(hass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_cover(hass):
|
async def test_cover_position_range(hass):
|
||||||
"""Test cover discovery."""
|
"""Test cover discovery and position using rangeController."""
|
||||||
device = (
|
device = (
|
||||||
"cover.test",
|
"cover.test_range",
|
||||||
"off",
|
"open",
|
||||||
{"friendly_name": "Test cover", "supported_features": 255, "position": 30},
|
{
|
||||||
|
"friendly_name": "Test cover range",
|
||||||
|
"device_class": "blind",
|
||||||
|
"supported_features": 7,
|
||||||
|
"position": 30,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
appliance = await discovery_test(device, hass)
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
assert appliance["endpointId"] == "cover#test"
|
assert appliance["endpointId"] == "cover#test_range"
|
||||||
assert appliance["displayCategories"][0] == "OTHER"
|
assert appliance["displayCategories"][0] == "INTERIOR_BLIND"
|
||||||
assert appliance["friendlyName"] == "Test cover"
|
assert appliance["friendlyName"] == "Test cover range"
|
||||||
|
|
||||||
assert_endpoint_capabilities(
|
capabilities = assert_endpoint_capabilities(
|
||||||
appliance,
|
appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa"
|
||||||
"Alexa.ModeController",
|
|
||||||
"Alexa.PercentageController",
|
|
||||||
"Alexa.PowerController",
|
|
||||||
"Alexa.EndpointHealth",
|
|
||||||
"Alexa",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await assert_power_controller_works(
|
range_capability = get_capability(capabilities, "Alexa.RangeController")
|
||||||
"cover#test", "cover.open_cover", "cover.close_cover", hass
|
assert range_capability is not None
|
||||||
)
|
assert range_capability["instance"] == "cover.position"
|
||||||
|
|
||||||
|
properties = range_capability["properties"]
|
||||||
|
assert properties["nonControllable"] is False
|
||||||
|
assert {"name": "rangeValue"} in properties["supported"]
|
||||||
|
|
||||||
|
capability_resources = range_capability["capabilityResources"]
|
||||||
|
assert capability_resources is not None
|
||||||
|
assert {
|
||||||
|
"@type": "text",
|
||||||
|
"value": {"text": "Position", "locale": "en-US"},
|
||||||
|
} in capability_resources["friendlyNames"]
|
||||||
|
|
||||||
|
assert {
|
||||||
|
"@type": "asset",
|
||||||
|
"value": {"assetId": "Alexa.Setting.Opening"},
|
||||||
|
} in capability_resources["friendlyNames"]
|
||||||
|
|
||||||
|
configuration = range_capability["configuration"]
|
||||||
|
assert configuration is not None
|
||||||
|
assert configuration["unitOfMeasure"] == "Alexa.Unit.Percent"
|
||||||
|
|
||||||
|
supported_range = configuration["supportedRange"]
|
||||||
|
assert supported_range["minimumValue"] == 0
|
||||||
|
assert supported_range["maximumValue"] == 100
|
||||||
|
assert supported_range["precision"] == 1
|
||||||
|
|
||||||
call, _ = await assert_request_calls_service(
|
call, _ = await assert_request_calls_service(
|
||||||
"Alexa.PercentageController",
|
"Alexa.RangeController",
|
||||||
"SetPercentage",
|
"SetRangeValue",
|
||||||
"cover#test",
|
"cover#test_range",
|
||||||
"cover.set_cover_position",
|
"cover.set_cover_position",
|
||||||
hass,
|
hass,
|
||||||
payload={"percentage": "50"},
|
payload={"rangeValue": "50"},
|
||||||
|
instance="cover.position",
|
||||||
)
|
)
|
||||||
assert call.data["position"] == 50
|
assert call.data["position"] == 50
|
||||||
|
|
||||||
await assert_percentage_changes(
|
call, msg = await assert_request_calls_service(
|
||||||
|
"Alexa.RangeController",
|
||||||
|
"SetRangeValue",
|
||||||
|
"cover#test_range",
|
||||||
|
"cover.close_cover",
|
||||||
hass,
|
hass,
|
||||||
[(25, "-5"), (35, "5"), (0, "-80")],
|
payload={"rangeValue": "0"},
|
||||||
"Alexa.PercentageController",
|
instance="cover.position",
|
||||||
"AdjustPercentage",
|
)
|
||||||
"cover#test",
|
properties = msg["context"]["properties"][0]
|
||||||
"percentageDelta",
|
assert properties["name"] == "rangeValue"
|
||||||
|
assert properties["namespace"] == "Alexa.RangeController"
|
||||||
|
assert properties["value"] == 0
|
||||||
|
|
||||||
|
call, msg = await assert_request_calls_service(
|
||||||
|
"Alexa.RangeController",
|
||||||
|
"SetRangeValue",
|
||||||
|
"cover#test_range",
|
||||||
|
"cover.open_cover",
|
||||||
|
hass,
|
||||||
|
payload={"rangeValue": "100"},
|
||||||
|
instance="cover.position",
|
||||||
|
)
|
||||||
|
properties = msg["context"]["properties"][0]
|
||||||
|
assert properties["name"] == "rangeValue"
|
||||||
|
assert properties["namespace"] == "Alexa.RangeController"
|
||||||
|
assert properties["value"] == 100
|
||||||
|
|
||||||
|
await assert_range_changes(
|
||||||
|
hass,
|
||||||
|
[(25, "-5"), (35, "5"), (0, "-99"), (100, "99")],
|
||||||
|
"Alexa.RangeController",
|
||||||
|
"AdjustRangeValue",
|
||||||
|
"cover#test_range",
|
||||||
|
False,
|
||||||
"cover.set_cover_position",
|
"cover.set_cover_position",
|
||||||
"position",
|
"position",
|
||||||
|
instance="cover.position",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -2292,26 +2389,25 @@ async def test_mode_unsupported_domain(hass):
|
|||||||
assert msg["payload"]["type"] == "INVALID_DIRECTIVE"
|
assert msg["payload"]["type"] == "INVALID_DIRECTIVE"
|
||||||
|
|
||||||
|
|
||||||
async def test_cover_position(hass):
|
async def test_cover_position_mode(hass):
|
||||||
"""Test cover position mode discovery."""
|
"""Test cover discovery and position using modeController."""
|
||||||
device = (
|
device = (
|
||||||
"cover.test",
|
"cover.test_mode",
|
||||||
"off",
|
"open",
|
||||||
{"friendly_name": "Test cover", "supported_features": 255, "position": 30},
|
{
|
||||||
|
"friendly_name": "Test cover mode",
|
||||||
|
"device_class": "blind",
|
||||||
|
"supported_features": 3,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
appliance = await discovery_test(device, hass)
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
assert appliance["endpointId"] == "cover#test"
|
assert appliance["endpointId"] == "cover#test_mode"
|
||||||
assert appliance["displayCategories"][0] == "OTHER"
|
assert appliance["displayCategories"][0] == "INTERIOR_BLIND"
|
||||||
assert appliance["friendlyName"] == "Test cover"
|
assert appliance["friendlyName"] == "Test cover mode"
|
||||||
|
|
||||||
capabilities = assert_endpoint_capabilities(
|
capabilities = assert_endpoint_capabilities(
|
||||||
appliance,
|
appliance, "Alexa", "Alexa.ModeController", "Alexa.EndpointHealth"
|
||||||
"Alexa",
|
|
||||||
"Alexa.ModeController",
|
|
||||||
"Alexa.PercentageController",
|
|
||||||
"Alexa.PowerController",
|
|
||||||
"Alexa.EndpointHealth",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
mode_capability = get_capability(capabilities, "Alexa.ModeController")
|
mode_capability = get_capability(capabilities, "Alexa.ModeController")
|
||||||
@ -2324,9 +2420,14 @@ async def test_cover_position(hass):
|
|||||||
|
|
||||||
capability_resources = mode_capability["capabilityResources"]
|
capability_resources = mode_capability["capabilityResources"]
|
||||||
assert capability_resources is not None
|
assert capability_resources is not None
|
||||||
|
assert {
|
||||||
|
"@type": "text",
|
||||||
|
"value": {"text": "Position", "locale": "en-US"},
|
||||||
|
} in capability_resources["friendlyNames"]
|
||||||
|
|
||||||
assert {
|
assert {
|
||||||
"@type": "asset",
|
"@type": "asset",
|
||||||
"value": {"assetId": "Alexa.Setting.Mode"},
|
"value": {"assetId": "Alexa.Setting.Opening"},
|
||||||
} in capability_resources["friendlyNames"]
|
} in capability_resources["friendlyNames"]
|
||||||
|
|
||||||
configuration = mode_capability["configuration"]
|
configuration = mode_capability["configuration"]
|
||||||
@ -2339,10 +2440,7 @@ async def test_cover_position(hass):
|
|||||||
"value": "position.open",
|
"value": "position.open",
|
||||||
"modeResources": {
|
"modeResources": {
|
||||||
"friendlyNames": [
|
"friendlyNames": [
|
||||||
{"@type": "text", "value": {"text": "open", "locale": "en-US"}},
|
{"@type": "asset", "value": {"assetId": "Alexa.Value.Open"}}
|
||||||
{"@type": "text", "value": {"text": "opened", "locale": "en-US"}},
|
|
||||||
{"@type": "text", "value": {"text": "raise", "locale": "en-US"}},
|
|
||||||
{"@type": "text", "value": {"text": "raised", "locale": "en-US"}},
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
} in supported_modes
|
} in supported_modes
|
||||||
@ -2350,19 +2448,24 @@ async def test_cover_position(hass):
|
|||||||
"value": "position.closed",
|
"value": "position.closed",
|
||||||
"modeResources": {
|
"modeResources": {
|
||||||
"friendlyNames": [
|
"friendlyNames": [
|
||||||
{"@type": "text", "value": {"text": "close", "locale": "en-US"}},
|
{"@type": "asset", "value": {"assetId": "Alexa.Value.Close"}}
|
||||||
{"@type": "text", "value": {"text": "closed", "locale": "en-US"}},
|
|
||||||
{"@type": "text", "value": {"text": "shut", "locale": "en-US"}},
|
|
||||||
{"@type": "text", "value": {"text": "lower", "locale": "en-US"}},
|
|
||||||
{"@type": "text", "value": {"text": "lowered", "locale": "en-US"}},
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
} in supported_modes
|
} in supported_modes
|
||||||
|
|
||||||
|
semantics = mode_capability["semantics"]
|
||||||
|
assert semantics is not None
|
||||||
|
|
||||||
|
action_mappings = semantics["actionMappings"]
|
||||||
|
assert action_mappings is not None
|
||||||
|
|
||||||
|
state_mappings = semantics["stateMappings"]
|
||||||
|
assert state_mappings is not None
|
||||||
|
|
||||||
call, msg = await assert_request_calls_service(
|
call, msg = await assert_request_calls_service(
|
||||||
"Alexa.ModeController",
|
"Alexa.ModeController",
|
||||||
"SetMode",
|
"SetMode",
|
||||||
"cover#test",
|
"cover#test_mode",
|
||||||
"cover.close_cover",
|
"cover.close_cover",
|
||||||
hass,
|
hass,
|
||||||
payload={"mode": "position.closed"},
|
payload={"mode": "position.closed"},
|
||||||
@ -2376,7 +2479,7 @@ async def test_cover_position(hass):
|
|||||||
call, msg = await assert_request_calls_service(
|
call, msg = await assert_request_calls_service(
|
||||||
"Alexa.ModeController",
|
"Alexa.ModeController",
|
||||||
"SetMode",
|
"SetMode",
|
||||||
"cover#test",
|
"cover#test_mode",
|
||||||
"cover.open_cover",
|
"cover.open_cover",
|
||||||
hass,
|
hass,
|
||||||
payload={"mode": "position.open"},
|
payload={"mode": "position.open"},
|
||||||
@ -2387,6 +2490,20 @@ async def test_cover_position(hass):
|
|||||||
assert properties["namespace"] == "Alexa.ModeController"
|
assert properties["namespace"] == "Alexa.ModeController"
|
||||||
assert properties["value"] == "position.open"
|
assert properties["value"] == "position.open"
|
||||||
|
|
||||||
|
call, msg = await assert_request_calls_service(
|
||||||
|
"Alexa.ModeController",
|
||||||
|
"SetMode",
|
||||||
|
"cover#test_mode",
|
||||||
|
"cover.stop_cover",
|
||||||
|
hass,
|
||||||
|
payload={"mode": "position.custom"},
|
||||||
|
instance="cover.position",
|
||||||
|
)
|
||||||
|
properties = msg["context"]["properties"][0]
|
||||||
|
assert properties["name"] == "mode"
|
||||||
|
assert properties["namespace"] == "Alexa.ModeController"
|
||||||
|
assert properties["value"] == "position.custom"
|
||||||
|
|
||||||
|
|
||||||
async def test_image_processing(hass):
|
async def test_image_processing(hass):
|
||||||
"""Test image_processing discovery as event detection."""
|
"""Test image_processing discovery as event detection."""
|
||||||
@ -2467,3 +2584,159 @@ async def test_presence_sensor(hass):
|
|||||||
assert properties["proactivelyReported"] is True
|
assert properties["proactivelyReported"] is True
|
||||||
assert not properties["retrievable"]
|
assert not properties["retrievable"]
|
||||||
assert {"name": "humanPresenceDetectionState"} in properties["supported"]
|
assert {"name": "humanPresenceDetectionState"} in properties["supported"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cover_tilt_position_range(hass):
|
||||||
|
"""Test cover discovery and tilt position using rangeController."""
|
||||||
|
device = (
|
||||||
|
"cover.test_tilt_range",
|
||||||
|
"open",
|
||||||
|
{
|
||||||
|
"friendly_name": "Test cover tilt range",
|
||||||
|
"device_class": "blind",
|
||||||
|
"supported_features": 240,
|
||||||
|
"tilt_position": 30,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
|
assert appliance["endpointId"] == "cover#test_tilt_range"
|
||||||
|
assert appliance["displayCategories"][0] == "INTERIOR_BLIND"
|
||||||
|
assert appliance["friendlyName"] == "Test cover tilt range"
|
||||||
|
|
||||||
|
capabilities = assert_endpoint_capabilities(
|
||||||
|
appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa"
|
||||||
|
)
|
||||||
|
|
||||||
|
range_capability = get_capability(capabilities, "Alexa.RangeController")
|
||||||
|
assert range_capability is not None
|
||||||
|
assert range_capability["instance"] == "cover.tilt_position"
|
||||||
|
|
||||||
|
semantics = range_capability["semantics"]
|
||||||
|
assert semantics is not None
|
||||||
|
|
||||||
|
action_mappings = semantics["actionMappings"]
|
||||||
|
assert action_mappings is not None
|
||||||
|
|
||||||
|
state_mappings = semantics["stateMappings"]
|
||||||
|
assert state_mappings is not None
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
"Alexa.RangeController",
|
||||||
|
"SetRangeValue",
|
||||||
|
"cover#test_tilt_range",
|
||||||
|
"cover.set_cover_tilt_position",
|
||||||
|
hass,
|
||||||
|
payload={"rangeValue": "50"},
|
||||||
|
instance="cover.tilt_position",
|
||||||
|
)
|
||||||
|
assert call.data["position"] == 50
|
||||||
|
|
||||||
|
call, msg = await assert_request_calls_service(
|
||||||
|
"Alexa.RangeController",
|
||||||
|
"SetRangeValue",
|
||||||
|
"cover#test_tilt_range",
|
||||||
|
"cover.close_cover_tilt",
|
||||||
|
hass,
|
||||||
|
payload={"rangeValue": "0"},
|
||||||
|
instance="cover.tilt_position",
|
||||||
|
)
|
||||||
|
properties = msg["context"]["properties"][0]
|
||||||
|
assert properties["name"] == "rangeValue"
|
||||||
|
assert properties["namespace"] == "Alexa.RangeController"
|
||||||
|
assert properties["value"] == 0
|
||||||
|
|
||||||
|
call, msg = await assert_request_calls_service(
|
||||||
|
"Alexa.RangeController",
|
||||||
|
"SetRangeValue",
|
||||||
|
"cover#test_tilt_range",
|
||||||
|
"cover.open_cover_tilt",
|
||||||
|
hass,
|
||||||
|
payload={"rangeValue": "100"},
|
||||||
|
instance="cover.tilt_position",
|
||||||
|
)
|
||||||
|
properties = msg["context"]["properties"][0]
|
||||||
|
assert properties["name"] == "rangeValue"
|
||||||
|
assert properties["namespace"] == "Alexa.RangeController"
|
||||||
|
assert properties["value"] == 100
|
||||||
|
|
||||||
|
await assert_range_changes(
|
||||||
|
hass,
|
||||||
|
[(25, "-5"), (35, "5"), (0, "-99"), (100, "99")],
|
||||||
|
"Alexa.RangeController",
|
||||||
|
"AdjustRangeValue",
|
||||||
|
"cover#test_tilt_range",
|
||||||
|
False,
|
||||||
|
"cover.set_cover_tilt_position",
|
||||||
|
"tilt_position",
|
||||||
|
instance="cover.tilt_position",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cover_semantics(hass):
|
||||||
|
"""Test cover discovery and semantics."""
|
||||||
|
device = (
|
||||||
|
"cover.test_semantics",
|
||||||
|
"open",
|
||||||
|
{
|
||||||
|
"friendly_name": "Test cover semantics",
|
||||||
|
"device_class": "blind",
|
||||||
|
"supported_features": 255,
|
||||||
|
"position": 30,
|
||||||
|
"tilt_position": 30,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
|
assert appliance["endpointId"] == "cover#test_semantics"
|
||||||
|
assert appliance["displayCategories"][0] == "INTERIOR_BLIND"
|
||||||
|
assert appliance["friendlyName"] == "Test cover semantics"
|
||||||
|
|
||||||
|
capabilities = assert_endpoint_capabilities(
|
||||||
|
appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa"
|
||||||
|
)
|
||||||
|
|
||||||
|
for range_instance in ("cover.position", "cover.tilt_position"):
|
||||||
|
range_capability = get_capability(
|
||||||
|
capabilities, "Alexa.RangeController", range_instance
|
||||||
|
)
|
||||||
|
semantics = range_capability["semantics"]
|
||||||
|
assert semantics is not None
|
||||||
|
|
||||||
|
action_mappings = semantics["actionMappings"]
|
||||||
|
assert action_mappings is not None
|
||||||
|
if range_instance == "cover.position":
|
||||||
|
assert {
|
||||||
|
"@type": "ActionsToDirective",
|
||||||
|
"actions": ["Alexa.Actions.Lower"],
|
||||||
|
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||||
|
} in action_mappings
|
||||||
|
assert {
|
||||||
|
"@type": "ActionsToDirective",
|
||||||
|
"actions": ["Alexa.Actions.Raise"],
|
||||||
|
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||||
|
} in action_mappings
|
||||||
|
elif range_instance == "cover.position":
|
||||||
|
assert {
|
||||||
|
"@type": "ActionsToDirective",
|
||||||
|
"actions": ["Alexa.Actions.Close"],
|
||||||
|
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||||
|
} in action_mappings
|
||||||
|
assert {
|
||||||
|
"@type": "ActionsToDirective",
|
||||||
|
"actions": ["Alexa.Actions.Open"],
|
||||||
|
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||||
|
} in action_mappings
|
||||||
|
|
||||||
|
state_mappings = semantics["stateMappings"]
|
||||||
|
assert state_mappings is not None
|
||||||
|
assert {
|
||||||
|
"@type": "StatesToValue",
|
||||||
|
"states": ["Alexa.States.Closed"],
|
||||||
|
"value": 0,
|
||||||
|
} in state_mappings
|
||||||
|
assert {
|
||||||
|
"@type": "StatesToRange",
|
||||||
|
"states": ["Alexa.States.Open"],
|
||||||
|
"range": {"minimumValue": 1, "maximumValue": 100},
|
||||||
|
} in state_mappings
|
||||||
|
Loading…
x
Reference in New Issue
Block a user