Implement ToggleController, RangeController, and ModeController in alexa (#27302)
* Implement AlexaToggleController, AlexaRangeController, and AlexaModeController interfaces. * Implement AlexaToggleController, AlexaRangeController, and AlexaModeController interfaces. * Unkerfuffled comments to please the pydocstyle gods. * Unkerfuffled comments in Tests to please the pydocstyle gods. * Added additional test for more coverage. * Removed OSCILLATING property check from from ModeController. * Added capability report tests for ModeController, ToggleController, RangeController, PowerLevelController. * Update homeassistant/components/alexa/capabilities.py Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io> * Update homeassistant/components/alexa/capabilities.py Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io> * Corrected mis-spelling of AlexaCapability class. * Changed instance from method to property in AlexaCapability class. * Refactored to add {entity.domain}.{entity.attribute} to the instance name. * Improved type handling for configuration object. Added additional test for configuration object. * Added Tests for unsupported domains for ModeController and RangeController * Made changes to improve future scaling for other domains. * Split fan range to speed maps into multiple constants.
This commit is contained in:
parent
dc3aa43f73
commit
da094e09fa
10 changed files with 1190 additions and 53 deletions
|
@ -23,19 +23,20 @@ import homeassistant.util.color as color_util
|
|||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import (
|
||||
Catalog,
|
||||
API_TEMP_UNITS,
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
DATE_FORMAT,
|
||||
PERCENTAGE_FAN_MAP,
|
||||
RANGE_FAN_MAP,
|
||||
)
|
||||
from .errors import UnsupportedProperty
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AlexaCapibility:
|
||||
class AlexaCapability:
|
||||
"""Base class for Alexa capability interfaces.
|
||||
|
||||
The Smart Home Skills API defines a number of "capability interfaces",
|
||||
|
@ -45,9 +46,10 @@ class AlexaCapibility:
|
|||
https://developer.amazon.com/docs/device-apis/message-guide.html
|
||||
"""
|
||||
|
||||
def __init__(self, entity):
|
||||
"""Initialize an Alexa capibility."""
|
||||
def __init__(self, entity, instance=None):
|
||||
"""Initialize an Alexa capability."""
|
||||
self.entity = entity
|
||||
self.instance = instance
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
|
@ -68,6 +70,11 @@ class AlexaCapibility:
|
|||
"""Return True if properties can be retrieved."""
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def properties_non_controllable():
|
||||
"""Return True if non controllable."""
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_property(name):
|
||||
"""Read and return a property.
|
||||
|
@ -84,9 +91,14 @@ class AlexaCapibility:
|
|||
"""Applicable only to scenes."""
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def capability_resources():
|
||||
"""Applicable to ToggleController, RangeController, and ModeController interfaces."""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def configuration():
|
||||
"""Applicable only to security control panel."""
|
||||
"""Return the Configuration object."""
|
||||
return []
|
||||
|
||||
def serialize_discovery(self):
|
||||
|
@ -102,15 +114,29 @@ class AlexaCapibility:
|
|||
},
|
||||
}
|
||||
|
||||
# pylint: disable=assignment-from-none
|
||||
non_controllable = self.properties_non_controllable()
|
||||
if non_controllable is not None:
|
||||
result["properties"]["nonControllable"] = non_controllable
|
||||
|
||||
# pylint: disable=assignment-from-none
|
||||
supports_deactivation = self.supports_deactivation()
|
||||
if supports_deactivation is not None:
|
||||
result["supportsDeactivation"] = supports_deactivation
|
||||
|
||||
capability_resources = self.serialize_capability_resources()
|
||||
if capability_resources:
|
||||
result["capabilityResources"] = capability_resources
|
||||
|
||||
configuration = self.configuration()
|
||||
if configuration:
|
||||
result["configuration"] = configuration
|
||||
|
||||
# pylint: disable=assignment-from-none
|
||||
instance = self.instance
|
||||
if instance is not None:
|
||||
result["instance"] = instance
|
||||
|
||||
return result
|
||||
|
||||
def serialize_properties(self):
|
||||
|
@ -120,16 +146,51 @@ class AlexaCapibility:
|
|||
# pylint: disable=assignment-from-no-return
|
||||
prop_value = self.get_property(prop_name)
|
||||
if prop_value is not None:
|
||||
yield {
|
||||
result = {
|
||||
"name": prop_name,
|
||||
"namespace": self.name(),
|
||||
"value": prop_value,
|
||||
"timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT),
|
||||
"uncertaintyInMilliseconds": 0,
|
||||
}
|
||||
instance = self.instance
|
||||
if instance is not None:
|
||||
result["instance"] = instance
|
||||
|
||||
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 AlexaEndpointHealth(AlexaCapibility):
|
||||
class AlexaEndpointHealth(AlexaCapability):
|
||||
"""Implements Alexa.EndpointHealth.
|
||||
|
||||
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it
|
||||
|
@ -166,7 +227,7 @@ class AlexaEndpointHealth(AlexaCapibility):
|
|||
return {"value": "OK"}
|
||||
|
||||
|
||||
class AlexaPowerController(AlexaCapibility):
|
||||
class AlexaPowerController(AlexaCapability):
|
||||
"""Implements Alexa.PowerController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-powercontroller.html
|
||||
|
@ -202,7 +263,7 @@ class AlexaPowerController(AlexaCapibility):
|
|||
return "ON" if is_on else "OFF"
|
||||
|
||||
|
||||
class AlexaLockController(AlexaCapibility):
|
||||
class AlexaLockController(AlexaCapability):
|
||||
"""Implements Alexa.LockController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-lockcontroller.html
|
||||
|
@ -236,7 +297,7 @@ class AlexaLockController(AlexaCapibility):
|
|||
return "JAMMED"
|
||||
|
||||
|
||||
class AlexaSceneController(AlexaCapibility):
|
||||
class AlexaSceneController(AlexaCapability):
|
||||
"""Implements Alexa.SceneController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-scenecontroller.html
|
||||
|
@ -252,7 +313,7 @@ class AlexaSceneController(AlexaCapibility):
|
|||
return "Alexa.SceneController"
|
||||
|
||||
|
||||
class AlexaBrightnessController(AlexaCapibility):
|
||||
class AlexaBrightnessController(AlexaCapability):
|
||||
"""Implements Alexa.BrightnessController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-brightnesscontroller.html
|
||||
|
@ -283,7 +344,7 @@ class AlexaBrightnessController(AlexaCapibility):
|
|||
return 0
|
||||
|
||||
|
||||
class AlexaColorController(AlexaCapibility):
|
||||
class AlexaColorController(AlexaCapability):
|
||||
"""Implements Alexa.ColorController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-colorcontroller.html
|
||||
|
@ -315,7 +376,7 @@ class AlexaColorController(AlexaCapibility):
|
|||
}
|
||||
|
||||
|
||||
class AlexaColorTemperatureController(AlexaCapibility):
|
||||
class AlexaColorTemperatureController(AlexaCapability):
|
||||
"""Implements Alexa.ColorTemperatureController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-colortemperaturecontroller.html
|
||||
|
@ -344,7 +405,7 @@ class AlexaColorTemperatureController(AlexaCapibility):
|
|||
return None
|
||||
|
||||
|
||||
class AlexaPercentageController(AlexaCapibility):
|
||||
class AlexaPercentageController(AlexaCapability):
|
||||
"""Implements Alexa.PercentageController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-percentagecontroller.html
|
||||
|
@ -378,7 +439,7 @@ class AlexaPercentageController(AlexaCapibility):
|
|||
return 0
|
||||
|
||||
|
||||
class AlexaSpeaker(AlexaCapibility):
|
||||
class AlexaSpeaker(AlexaCapability):
|
||||
"""Implements Alexa.Speaker.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-speaker.html
|
||||
|
@ -389,7 +450,7 @@ class AlexaSpeaker(AlexaCapibility):
|
|||
return "Alexa.Speaker"
|
||||
|
||||
|
||||
class AlexaStepSpeaker(AlexaCapibility):
|
||||
class AlexaStepSpeaker(AlexaCapability):
|
||||
"""Implements Alexa.StepSpeaker.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html
|
||||
|
@ -400,7 +461,7 @@ class AlexaStepSpeaker(AlexaCapibility):
|
|||
return "Alexa.StepSpeaker"
|
||||
|
||||
|
||||
class AlexaPlaybackController(AlexaCapibility):
|
||||
class AlexaPlaybackController(AlexaCapability):
|
||||
"""Implements Alexa.PlaybackController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html
|
||||
|
@ -411,7 +472,7 @@ class AlexaPlaybackController(AlexaCapibility):
|
|||
return "Alexa.PlaybackController"
|
||||
|
||||
|
||||
class AlexaInputController(AlexaCapibility):
|
||||
class AlexaInputController(AlexaCapability):
|
||||
"""Implements Alexa.InputController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html
|
||||
|
@ -422,7 +483,7 @@ class AlexaInputController(AlexaCapibility):
|
|||
return "Alexa.InputController"
|
||||
|
||||
|
||||
class AlexaTemperatureSensor(AlexaCapibility):
|
||||
class AlexaTemperatureSensor(AlexaCapability):
|
||||
"""Implements Alexa.TemperatureSensor.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-temperaturesensor.html
|
||||
|
@ -472,7 +533,7 @@ class AlexaTemperatureSensor(AlexaCapibility):
|
|||
return {"value": temp, "scale": API_TEMP_UNITS[unit]}
|
||||
|
||||
|
||||
class AlexaContactSensor(AlexaCapibility):
|
||||
class AlexaContactSensor(AlexaCapability):
|
||||
"""Implements Alexa.ContactSensor.
|
||||
|
||||
The Alexa.ContactSensor interface describes the properties and events used
|
||||
|
@ -514,7 +575,7 @@ class AlexaContactSensor(AlexaCapibility):
|
|||
return "NOT_DETECTED"
|
||||
|
||||
|
||||
class AlexaMotionSensor(AlexaCapibility):
|
||||
class AlexaMotionSensor(AlexaCapability):
|
||||
"""Implements Alexa.MotionSensor.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html
|
||||
|
@ -551,7 +612,7 @@ class AlexaMotionSensor(AlexaCapibility):
|
|||
return "NOT_DETECTED"
|
||||
|
||||
|
||||
class AlexaThermostatController(AlexaCapibility):
|
||||
class AlexaThermostatController(AlexaCapability):
|
||||
"""Implements Alexa.ThermostatController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-thermostatcontroller.html
|
||||
|
@ -631,7 +692,7 @@ class AlexaThermostatController(AlexaCapibility):
|
|||
return {"value": temp, "scale": API_TEMP_UNITS[unit]}
|
||||
|
||||
|
||||
class AlexaPowerLevelController(AlexaCapibility):
|
||||
class AlexaPowerLevelController(AlexaCapability):
|
||||
"""Implements Alexa.PowerLevelController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-powerlevelcontroller.html
|
||||
|
@ -666,7 +727,7 @@ class AlexaPowerLevelController(AlexaCapibility):
|
|||
return None
|
||||
|
||||
|
||||
class AlexaSecurityPanelController(AlexaCapibility):
|
||||
class AlexaSecurityPanelController(AlexaCapability):
|
||||
"""Implements Alexa.SecurityPanelController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html
|
||||
|
@ -710,9 +771,271 @@ class AlexaSecurityPanelController(AlexaCapibility):
|
|||
return "DISARMED"
|
||||
|
||||
def configuration(self):
|
||||
"""Return supported authorization types."""
|
||||
"""Return configuration object with supported authorization types."""
|
||||
code_format = self.entity.attributes.get(ATTR_CODE_FORMAT)
|
||||
|
||||
if code_format == FORMAT_NUMBER:
|
||||
return {"supportedAuthorizationTypes": [{"type": "FOUR_DIGIT_PIN"}]}
|
||||
return []
|
||||
return None
|
||||
|
||||
|
||||
class AlexaModeController(AlexaCapability):
|
||||
"""Implements Alexa.ModeController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html
|
||||
"""
|
||||
|
||||
def __init__(self, entity, instance, non_controllable=False):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity, instance)
|
||||
self.properties_non_controllable = lambda: non_controllable
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return "Alexa.ModeController"
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{"name": "mode"}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != "mode":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||
return self.entity.attributes.get(fan.ATTR_DIRECTION)
|
||||
|
||||
return None
|
||||
|
||||
def configuration(self):
|
||||
"""Return configuration with modeResources."""
|
||||
return self.serialize_mode_resources()
|
||||
|
||||
def capability_resources(self):
|
||||
"""Return capabilityResources object."""
|
||||
capability_resources = []
|
||||
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||
capability_resources = [
|
||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION}
|
||||
]
|
||||
|
||||
return capability_resources
|
||||
|
||||
def mode_resources(self):
|
||||
"""Return modeResources 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}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return mode_resources
|
||||
|
||||
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):
|
||||
"""Implements Alexa.RangeController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html
|
||||
"""
|
||||
|
||||
def __init__(self, entity, instance, non_controllable=False):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity, instance)
|
||||
self.properties_non_controllable = lambda: non_controllable
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return "Alexa.RangeController"
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{"name": "rangeValue"}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != "rangeValue":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
speed = self.entity.attributes.get(fan.ATTR_SPEED)
|
||||
return RANGE_FAN_MAP.get(speed, 0)
|
||||
|
||||
return None
|
||||
|
||||
def configuration(self):
|
||||
"""Return configuration with presetResources."""
|
||||
return self.serialize_preset_resources()
|
||||
|
||||
def capability_resources(self):
|
||||
"""Return capabilityResources object."""
|
||||
capability_resources = []
|
||||
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
return [{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_FANSPEED}]
|
||||
|
||||
return capability_resources
|
||||
|
||||
def preset_resources(self):
|
||||
"""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"])
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"supportedRange": {
|
||||
"minimumValue": resources["minimumValue"],
|
||||
"maximumValue": resources["maximumValue"],
|
||||
"precision": resources["precision"],
|
||||
},
|
||||
"presets": preset_resources,
|
||||
}
|
||||
|
||||
|
||||
class AlexaToggleController(AlexaCapability):
|
||||
"""Implements Alexa.ToggleController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html
|
||||
"""
|
||||
|
||||
def __init__(self, entity, instance, non_controllable=False):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity, instance)
|
||||
self.properties_non_controllable = lambda: non_controllable
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return "Alexa.ToggleController"
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{"name": "toggleState"}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != "toggleState":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||
is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING))
|
||||
return "ON" if is_on else "OFF"
|
||||
|
||||
return None
|
||||
|
||||
def capability_resources(self):
|
||||
"""Return capabilityResources object."""
|
||||
capability_resources = []
|
||||
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||
capability_resources = [
|
||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_OSCILLATE},
|
||||
{"type": Catalog.LABEL_TEXT, "value": "Rotate"},
|
||||
{"type": Catalog.LABEL_TEXT, "value": "Rotation"},
|
||||
]
|
||||
|
||||
return capability_resources
|
||||
|
|
|
@ -5,7 +5,6 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
|||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.components import fan
|
||||
|
||||
|
||||
DOMAIN = "alexa"
|
||||
|
||||
# Flash briefing constants
|
||||
|
@ -69,6 +68,20 @@ PERCENTAGE_FAN_MAP = {
|
|||
fan.SPEED_HIGH: 100,
|
||||
}
|
||||
|
||||
RANGE_FAN_MAP = {
|
||||
fan.SPEED_OFF: 0,
|
||||
fan.SPEED_LOW: 1,
|
||||
fan.SPEED_MEDIUM: 2,
|
||||
fan.SPEED_HIGH: 3,
|
||||
}
|
||||
|
||||
SPEED_FAN_MAP = {
|
||||
0: fan.SPEED_OFF,
|
||||
1: fan.SPEED_LOW,
|
||||
2: fan.SPEED_MEDIUM,
|
||||
3: fan.SPEED_HIGH,
|
||||
}
|
||||
|
||||
|
||||
class Cause:
|
||||
"""Possible causes for property changes.
|
||||
|
@ -101,3 +114,160 @@ class Cause:
|
|||
# Indicates that the event was caused by a voice interaction with Alexa.
|
||||
# For example a user speaking to their Echo device.
|
||||
VOICE_INTERACTION = "VOICE_INTERACTION"
|
||||
|
||||
|
||||
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"
|
||||
|
|
|
@ -40,17 +40,20 @@ from .capabilities import (
|
|||
AlexaEndpointHealth,
|
||||
AlexaInputController,
|
||||
AlexaLockController,
|
||||
AlexaModeController,
|
||||
AlexaMotionSensor,
|
||||
AlexaPercentageController,
|
||||
AlexaPlaybackController,
|
||||
AlexaPowerController,
|
||||
AlexaPowerLevelController,
|
||||
AlexaRangeController,
|
||||
AlexaSceneController,
|
||||
AlexaSecurityPanelController,
|
||||
AlexaSpeaker,
|
||||
AlexaStepSpeaker,
|
||||
AlexaTemperatureSensor,
|
||||
AlexaThermostatController,
|
||||
AlexaToggleController,
|
||||
)
|
||||
|
||||
ENTITY_ADAPTERS = Registry()
|
||||
|
@ -348,6 +351,19 @@ class FanCapabilities(AlexaEntity):
|
|||
if supported & fan.SUPPORT_SET_SPEED:
|
||||
yield AlexaPercentageController(self.entity)
|
||||
yield AlexaPowerLevelController(self.entity)
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}"
|
||||
)
|
||||
|
||||
if supported & fan.SUPPORT_OSCILLATE:
|
||||
yield AlexaToggleController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
|
||||
)
|
||||
if supported & fan.SUPPORT_DIRECTION:
|
||||
yield AlexaModeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}"
|
||||
)
|
||||
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
|
||||
|
|
|
@ -97,3 +97,17 @@ class AlexaSecurityPanelAuthorizationRequired(AlexaError):
|
|||
|
||||
namespace = "Alexa.SecurityPanelController"
|
||||
error_type = "AUTHORIZATION_REQUIRED"
|
||||
|
||||
|
||||
class AlexaAlreadyInOperationError(AlexaError):
|
||||
"""Class to represent AlreadyInOperation errors."""
|
||||
|
||||
namespace = "Alexa"
|
||||
error_type = "ALREADY_IN_OPERATION"
|
||||
|
||||
|
||||
class AlexaInvalidDirectiveError(AlexaError):
|
||||
"""Class to represent InvalidDirective errors."""
|
||||
|
||||
namespace = "Alexa"
|
||||
error_type = "INVALID_DIRECTIVE"
|
||||
|
|
|
@ -36,9 +36,18 @@ import homeassistant.util.dt as dt_util
|
|||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
from .const import API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause
|
||||
from .const import (
|
||||
API_TEMP_UNITS,
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
Cause,
|
||||
PERCENTAGE_FAN_MAP,
|
||||
RANGE_FAN_MAP,
|
||||
SPEED_FAN_MAP,
|
||||
)
|
||||
from .entities import async_get_entities
|
||||
from .errors import (
|
||||
AlexaInvalidDirectiveError,
|
||||
AlexaInvalidValueError,
|
||||
AlexaSecurityPanelAuthorizationRequired,
|
||||
AlexaSecurityPanelUnauthorizedError,
|
||||
|
@ -356,15 +365,7 @@ async def async_api_adjust_percentage(hass, config, directive, context):
|
|||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = entity.attributes.get(fan.ATTR_SPEED)
|
||||
|
||||
if speed == "off":
|
||||
current = 0
|
||||
elif speed == "low":
|
||||
current = 33
|
||||
elif speed == "medium":
|
||||
current = 66
|
||||
elif speed == "high":
|
||||
current = 100
|
||||
current = PERCENTAGE_FAN_MAP.get(speed, 100)
|
||||
|
||||
# set percentage
|
||||
percentage = max(0, percentage_delta + current)
|
||||
|
@ -827,20 +828,11 @@ async def async_api_adjust_power_level(hass, config, directive, context):
|
|||
percentage_delta = int(directive.payload["powerLevelDelta"])
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
current = 0
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = entity.attributes.get(fan.ATTR_SPEED)
|
||||
|
||||
if speed == "off":
|
||||
current = 0
|
||||
elif speed == "low":
|
||||
current = 33
|
||||
elif speed == "medium":
|
||||
current = 66
|
||||
else:
|
||||
current = 100
|
||||
current = PERCENTAGE_FAN_MAP.get(speed, 100)
|
||||
|
||||
# set percentage
|
||||
percentage = max(0, percentage_delta + current)
|
||||
|
@ -928,3 +920,165 @@ async def async_api_disarm(hass, config, directive, context):
|
|||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.ModeController", "SetMode"))
|
||||
async def async_api_set_mode(hass, config, directive, context):
|
||||
"""Process a next request."""
|
||||
entity = directive.entity
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
mode = directive.payload["mode"]
|
||||
|
||||
if domain != fan.DOMAIN:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||
mode, direction = mode.split(".")
|
||||
if direction in [fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD]:
|
||||
service = fan.SERVICE_SET_DIRECTION
|
||||
data[fan.ATTR_DIRECTION] = direction
|
||||
|
||||
await hass.services.async_call(
|
||||
domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.ModeController", "AdjustMode"))
|
||||
async def async_api_adjust_mode(hass, config, directive, context):
|
||||
"""Process a AdjustMode request.
|
||||
|
||||
Requires modeResources to be ordered.
|
||||
Only modes that are ordered support the adjustMode directive.
|
||||
"""
|
||||
entity = directive.entity
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
|
||||
if domain != fan.DOMAIN:
|
||||
msg = "Entity does not support directive"
|
||||
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"))
|
||||
async def async_api_toggle_on(hass, config, directive, context):
|
||||
"""Process a toggle on request."""
|
||||
entity = directive.entity
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if domain != fan.DOMAIN:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||
service = fan.SERVICE_OSCILLATE
|
||||
data[fan.ATTR_OSCILLATING] = True
|
||||
|
||||
await hass.services.async_call(
|
||||
domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.ToggleController", "TurnOff"))
|
||||
async def async_api_toggle_off(hass, config, directive, context):
|
||||
"""Process a toggle off request."""
|
||||
entity = directive.entity
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if domain != fan.DOMAIN:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||
service = fan.SERVICE_OSCILLATE
|
||||
data[fan.ATTR_OSCILLATING] = False
|
||||
|
||||
await hass.services.async_call(
|
||||
domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.RangeController", "SetRangeValue"))
|
||||
async def async_api_set_range(hass, config, directive, context):
|
||||
"""Process a next request."""
|
||||
entity = directive.entity
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
range_value = int(directive.payload["rangeValue"])
|
||||
|
||||
if domain != fan.DOMAIN:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = SPEED_FAN_MAP.get(range_value, None)
|
||||
|
||||
if not speed:
|
||||
msg = "Entity does not support value"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
if speed == fan.SPEED_OFF:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
await hass.services.async_call(
|
||||
domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue"))
|
||||
async def async_api_adjust_range(hass, config, directive, context):
|
||||
"""Process a next request."""
|
||||
entity = directive.entity
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
range_delta = int(directive.payload["rangeValueDelta"])
|
||||
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
|
||||
# adjust range
|
||||
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)
|
||||
|
||||
if speed == fan.SPEED_OFF:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
await hass.services.async_call(
|
||||
domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
|
|
@ -28,7 +28,7 @@ class AlexaDirective:
|
|||
self.payload = self._directive[API_PAYLOAD]
|
||||
self.has_endpoint = API_ENDPOINT in self._directive
|
||||
|
||||
self.entity = self.entity_id = self.endpoint = None
|
||||
self.entity = self.entity_id = self.endpoint = self.instance = None
|
||||
|
||||
def load_entity(self, hass, config):
|
||||
"""Set attributes related to the entity for this request.
|
||||
|
@ -38,6 +38,7 @@ class AlexaDirective:
|
|||
- entity
|
||||
- entity_id
|
||||
- endpoint
|
||||
- instance (when header includes instance property)
|
||||
|
||||
Behavior when self.has_endpoint is False is undefined.
|
||||
|
||||
|
@ -52,6 +53,8 @@ class AlexaDirective:
|
|||
raise AlexaInvalidEndpointError(_endpoint_id)
|
||||
|
||||
self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity)
|
||||
if "instance" in self._directive[API_HEADER]:
|
||||
self.instance = self._directive[API_HEADER]["instance"]
|
||||
|
||||
def response(self, name="Response", namespace="Alexa", payload=None):
|
||||
"""Create an API formatted response.
|
||||
|
|
|
@ -67,13 +67,22 @@ def get_new_request(namespace, name, endpoint=None):
|
|||
|
||||
|
||||
async def assert_request_calls_service(
|
||||
namespace, name, endpoint, service, hass, response_type="Response", payload=None
|
||||
namespace,
|
||||
name,
|
||||
endpoint,
|
||||
service,
|
||||
hass,
|
||||
response_type="Response",
|
||||
payload=None,
|
||||
instance=None,
|
||||
):
|
||||
"""Assert an API request calls a hass service."""
|
||||
context = Context()
|
||||
request = get_new_request(namespace, name, endpoint)
|
||||
if payload:
|
||||
request["directive"]["payload"] = payload
|
||||
if instance:
|
||||
request["directive"]["header"]["instance"] = instance
|
||||
|
||||
domain, service_name = service.split(".")
|
||||
calls = async_mock_service(hass, domain, service_name)
|
||||
|
|
|
@ -305,7 +305,7 @@ async def test_report_colored_temp_light_state(hass):
|
|||
|
||||
|
||||
async def test_report_fan_speed_state(hass):
|
||||
"""Test PercentageController reports fan speed correctly."""
|
||||
"""Test PercentageController, PowerLevelController, RangeController reports fan speed correctly."""
|
||||
hass.states.async_set(
|
||||
"fan.off",
|
||||
"off",
|
||||
|
@ -333,15 +333,82 @@ async def test_report_fan_speed_state(hass):
|
|||
|
||||
properties = await reported_properties(hass, "fan.off")
|
||||
properties.assert_equal("Alexa.PercentageController", "percentage", 0)
|
||||
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 0)
|
||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 0)
|
||||
|
||||
properties = await reported_properties(hass, "fan.low_speed")
|
||||
properties.assert_equal("Alexa.PercentageController", "percentage", 33)
|
||||
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 33)
|
||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 1)
|
||||
|
||||
properties = await reported_properties(hass, "fan.medium_speed")
|
||||
properties.assert_equal("Alexa.PercentageController", "percentage", 66)
|
||||
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 66)
|
||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 2)
|
||||
|
||||
properties = await reported_properties(hass, "fan.high_speed")
|
||||
properties.assert_equal("Alexa.PercentageController", "percentage", 100)
|
||||
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 100)
|
||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 3)
|
||||
|
||||
|
||||
async def test_report_fan_oscillating(hass):
|
||||
"""Test ToggleController reports fan oscillating correctly."""
|
||||
hass.states.async_set(
|
||||
"fan.off",
|
||||
"off",
|
||||
{"friendly_name": "Off fan", "speed": "off", "supported_features": 3},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"fan.low_speed",
|
||||
"on",
|
||||
{
|
||||
"friendly_name": "Low speed fan",
|
||||
"speed": "low",
|
||||
"oscillating": True,
|
||||
"supported_features": 3,
|
||||
},
|
||||
)
|
||||
|
||||
properties = await reported_properties(hass, "fan.off")
|
||||
properties.assert_equal("Alexa.ToggleController", "toggleState", "OFF")
|
||||
|
||||
properties = await reported_properties(hass, "fan.low_speed")
|
||||
properties.assert_equal("Alexa.ToggleController", "toggleState", "ON")
|
||||
|
||||
|
||||
async def test_report_fan_direction(hass):
|
||||
"""Test ModeController reports fan direction correctly."""
|
||||
hass.states.async_set(
|
||||
"fan.off", "off", {"friendly_name": "Off fan", "supported_features": 4}
|
||||
)
|
||||
hass.states.async_set(
|
||||
"fan.reverse",
|
||||
"on",
|
||||
{
|
||||
"friendly_name": "Fan Reverse",
|
||||
"direction": "reverse",
|
||||
"supported_features": 4,
|
||||
},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"fan.forward",
|
||||
"on",
|
||||
{
|
||||
"friendly_name": "Fan Forward",
|
||||
"direction": "forward",
|
||||
"supported_features": 4,
|
||||
},
|
||||
)
|
||||
|
||||
properties = await reported_properties(hass, "fan.off")
|
||||
properties.assert_not_has_property("Alexa.ModeController", "mode")
|
||||
|
||||
properties = await reported_properties(hass, "fan.reverse")
|
||||
properties.assert_equal("Alexa.ModeController", "mode", "reverse")
|
||||
|
||||
properties = await reported_properties(hass, "fan.forward")
|
||||
properties.assert_equal("Alexa.ModeController", "mode", "forward")
|
||||
|
||||
|
||||
async def test_report_cover_percentage_state(hass):
|
||||
|
|
|
@ -310,10 +310,14 @@ async def test_fan(hass):
|
|||
assert appliance["endpointId"] == "fan#test_1"
|
||||
assert appliance["displayCategories"][0] == "FAN"
|
||||
assert appliance["friendlyName"] == "Test fan 1"
|
||||
assert_endpoint_capabilities(
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance, "Alexa.PowerController", "Alexa.EndpointHealth"
|
||||
)
|
||||
|
||||
power_capability = get_capability(capabilities, "Alexa.PowerController")
|
||||
assert "capabilityResources" not in power_capability
|
||||
assert "configuration" not in power_capability
|
||||
|
||||
|
||||
async def test_variable_fan(hass):
|
||||
"""Test fan discovery.
|
||||
|
@ -336,14 +340,33 @@ async def test_variable_fan(hass):
|
|||
assert appliance["displayCategories"][0] == "FAN"
|
||||
assert appliance["friendlyName"] == "Test fan 2"
|
||||
|
||||
assert_endpoint_capabilities(
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance,
|
||||
"Alexa.PercentageController",
|
||||
"Alexa.PowerController",
|
||||
"Alexa.PowerLevelController",
|
||||
"Alexa.RangeController",
|
||||
"Alexa.EndpointHealth",
|
||||
)
|
||||
|
||||
range_capability = get_capability(capabilities, "Alexa.RangeController")
|
||||
assert range_capability is not None
|
||||
assert range_capability["instance"] == "fan.speed"
|
||||
|
||||
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": "asset",
|
||||
"value": {"assetId": "Alexa.Setting.FanSpeed"},
|
||||
} in capability_resources["friendlyNames"]
|
||||
|
||||
configuration = range_capability["configuration"]
|
||||
assert configuration is not None
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.PercentageController",
|
||||
"SetPercentage",
|
||||
|
@ -377,7 +400,7 @@ async def test_variable_fan(hass):
|
|||
|
||||
await assert_percentage_changes(
|
||||
hass,
|
||||
[("high", "-5"), ("high", "5"), ("low", "-80")],
|
||||
[("high", "-5"), ("medium", "-50"), ("low", "-80")],
|
||||
"Alexa.PowerLevelController",
|
||||
"AdjustPowerLevel",
|
||||
"fan#test_2",
|
||||
|
@ -387,6 +410,251 @@ async def test_variable_fan(hass):
|
|||
)
|
||||
|
||||
|
||||
async def test_oscillating_fan(hass):
|
||||
"""Test oscillating fan discovery."""
|
||||
device = (
|
||||
"fan.test_3",
|
||||
"off",
|
||||
{"friendly_name": "Test fan 3", "supported_features": 3},
|
||||
)
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
assert appliance["endpointId"] == "fan#test_3"
|
||||
assert appliance["displayCategories"][0] == "FAN"
|
||||
assert appliance["friendlyName"] == "Test fan 3"
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance,
|
||||
"Alexa.PercentageController",
|
||||
"Alexa.PowerController",
|
||||
"Alexa.PowerLevelController",
|
||||
"Alexa.RangeController",
|
||||
"Alexa.ToggleController",
|
||||
"Alexa.EndpointHealth",
|
||||
)
|
||||
|
||||
toggle_capability = get_capability(capabilities, "Alexa.ToggleController")
|
||||
assert toggle_capability is not None
|
||||
assert toggle_capability["instance"] == "fan.oscillating"
|
||||
|
||||
properties = toggle_capability["properties"]
|
||||
assert properties["nonControllable"] is False
|
||||
assert {"name": "toggleState"} in properties["supported"]
|
||||
|
||||
capability_resources = toggle_capability["capabilityResources"]
|
||||
assert capability_resources is not None
|
||||
assert {
|
||||
"@type": "asset",
|
||||
"value": {"assetId": "Alexa.Setting.Oscillate"},
|
||||
} in capability_resources["friendlyNames"]
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.ToggleController",
|
||||
"TurnOn",
|
||||
"fan#test_3",
|
||||
"fan.oscillate",
|
||||
hass,
|
||||
payload={},
|
||||
instance="fan.oscillating",
|
||||
)
|
||||
assert call.data["oscillating"]
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.ToggleController",
|
||||
"TurnOff",
|
||||
"fan#test_3",
|
||||
"fan.oscillate",
|
||||
hass,
|
||||
payload={},
|
||||
instance="fan.oscillating",
|
||||
)
|
||||
assert not call.data["oscillating"]
|
||||
|
||||
|
||||
async def test_direction_fan(hass):
|
||||
"""Test direction fan discovery."""
|
||||
device = (
|
||||
"fan.test_4",
|
||||
"on",
|
||||
{
|
||||
"friendly_name": "Test fan 4",
|
||||
"supported_features": 5,
|
||||
"direction": "forward",
|
||||
},
|
||||
)
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
assert appliance["endpointId"] == "fan#test_4"
|
||||
assert appliance["displayCategories"][0] == "FAN"
|
||||
assert appliance["friendlyName"] == "Test fan 4"
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance,
|
||||
"Alexa.PercentageController",
|
||||
"Alexa.PowerController",
|
||||
"Alexa.PowerLevelController",
|
||||
"Alexa.RangeController",
|
||||
"Alexa.ModeController",
|
||||
"Alexa.EndpointHealth",
|
||||
)
|
||||
|
||||
mode_capability = get_capability(capabilities, "Alexa.ModeController")
|
||||
assert mode_capability is not None
|
||||
assert mode_capability["instance"] == "fan.direction"
|
||||
|
||||
properties = mode_capability["properties"]
|
||||
assert properties["nonControllable"] is False
|
||||
assert {"name": "mode"} in properties["supported"]
|
||||
|
||||
capability_resources = mode_capability["capabilityResources"]
|
||||
assert capability_resources is not None
|
||||
assert {
|
||||
"@type": "asset",
|
||||
"value": {"assetId": "Alexa.Setting.Direction"},
|
||||
} in capability_resources["friendlyNames"]
|
||||
|
||||
configuration = mode_capability["configuration"]
|
||||
assert configuration is not None
|
||||
assert configuration["ordered"] is False
|
||||
|
||||
supported_modes = configuration["supportedModes"]
|
||||
assert supported_modes is not None
|
||||
assert {
|
||||
"value": "direction.forward",
|
||||
"modeResources": {
|
||||
"friendlyNames": [
|
||||
{"@type": "text", "value": {"text": "forward", "locale": "en-US"}}
|
||||
]
|
||||
},
|
||||
} in supported_modes
|
||||
assert {
|
||||
"value": "direction.reverse",
|
||||
"modeResources": {
|
||||
"friendlyNames": [
|
||||
{"@type": "text", "value": {"text": "reverse", "locale": "en-US"}}
|
||||
]
|
||||
},
|
||||
} in supported_modes
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.ModeController",
|
||||
"SetMode",
|
||||
"fan#test_4",
|
||||
"fan.set_direction",
|
||||
hass,
|
||||
payload={"mode": "direction.reverse"},
|
||||
instance="fan.direction",
|
||||
)
|
||||
assert call.data["direction"] == "reverse"
|
||||
|
||||
# Test for AdjustMode instance=None Error coverage
|
||||
with pytest.raises(AssertionError):
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.ModeController",
|
||||
"AdjustMode",
|
||||
"fan#test_4",
|
||||
"fan.set_direction",
|
||||
hass,
|
||||
payload={},
|
||||
instance=None,
|
||||
)
|
||||
assert call.data
|
||||
|
||||
|
||||
async def test_fan_range(hass):
|
||||
"""Test fan discovery with range controller.
|
||||
|
||||
This one has variable speed.
|
||||
"""
|
||||
device = (
|
||||
"fan.test_5",
|
||||
"off",
|
||||
{
|
||||
"friendly_name": "Test fan 5",
|
||||
"supported_features": 1,
|
||||
"speed_list": ["low", "medium", "high"],
|
||||
"speed": "medium",
|
||||
},
|
||||
)
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
assert appliance["endpointId"] == "fan#test_5"
|
||||
assert appliance["displayCategories"][0] == "FAN"
|
||||
assert appliance["friendlyName"] == "Test fan 5"
|
||||
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance,
|
||||
"Alexa.PercentageController",
|
||||
"Alexa.PowerController",
|
||||
"Alexa.PowerLevelController",
|
||||
"Alexa.RangeController",
|
||||
"Alexa.EndpointHealth",
|
||||
)
|
||||
|
||||
range_capability = get_capability(capabilities, "Alexa.RangeController")
|
||||
assert range_capability is not None
|
||||
assert range_capability["instance"] == "fan.speed"
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"fan#test_5",
|
||||
"fan.set_speed",
|
||||
hass,
|
||||
payload={"rangeValue": "1"},
|
||||
instance="fan.speed",
|
||||
)
|
||||
assert call.data["speed"] == "low"
|
||||
|
||||
await assert_range_changes(
|
||||
hass,
|
||||
[("low", "-1"), ("high", "1"), ("medium", "0")],
|
||||
"Alexa.RangeController",
|
||||
"AdjustRangeValue",
|
||||
"fan#test_5",
|
||||
False,
|
||||
"fan.set_speed",
|
||||
"speed",
|
||||
instance="fan.speed",
|
||||
)
|
||||
|
||||
|
||||
async def test_fan_range_off(hass):
|
||||
"""Test fan range controller 0 turns_off fan."""
|
||||
device = (
|
||||
"fan.test_6",
|
||||
"off",
|
||||
{
|
||||
"friendly_name": "Test fan 6",
|
||||
"supported_features": 1,
|
||||
"speed_list": ["low", "medium", "high"],
|
||||
"speed": "high",
|
||||
},
|
||||
)
|
||||
await discovery_test(device, hass)
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"fan#test_6",
|
||||
"fan.turn_off",
|
||||
hass,
|
||||
payload={"rangeValue": "0"},
|
||||
instance="fan.speed",
|
||||
)
|
||||
assert call.data["speed"] == "off"
|
||||
|
||||
await assert_range_changes(
|
||||
hass,
|
||||
[("off", "-3")],
|
||||
"Alexa.RangeController",
|
||||
"AdjustRangeValue",
|
||||
"fan#test_6",
|
||||
False,
|
||||
"fan.turn_off",
|
||||
"speed",
|
||||
instance="fan.speed",
|
||||
)
|
||||
|
||||
|
||||
async def test_lock(hass):
|
||||
"""Test lock discovery."""
|
||||
device = ("lock.test", "off", {"friendly_name": "Test lock"})
|
||||
|
@ -729,6 +997,33 @@ async def assert_percentage_changes(
|
|||
assert call.data[changed_parameter] == result_volume
|
||||
|
||||
|
||||
async def assert_range_changes(
|
||||
hass,
|
||||
adjustments,
|
||||
namespace,
|
||||
name,
|
||||
endpoint,
|
||||
delta_default,
|
||||
service,
|
||||
changed_parameter,
|
||||
instance,
|
||||
):
|
||||
"""Assert an API request making range changes works.
|
||||
|
||||
AdjustRangeValue are examples of such requests.
|
||||
"""
|
||||
for result_range, adjustment in adjustments:
|
||||
payload = {
|
||||
"rangeValueDelta": adjustment,
|
||||
"rangeValueDeltaDefault": delta_default,
|
||||
}
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
namespace, name, endpoint, service, hass, payload=payload, instance=instance
|
||||
)
|
||||
assert call.data[changed_parameter] == result_range
|
||||
|
||||
|
||||
async def test_temp_sensor(hass):
|
||||
"""Test temperature sensor discovery."""
|
||||
device = (
|
||||
|
@ -1438,3 +1733,41 @@ async def test_alarm_control_panel_code_arm_required(hass):
|
|||
{"friendly_name": "Test Alarm Control Panel 3", "code_arm_required": True},
|
||||
)
|
||||
await discovery_test(device, hass, expected_endpoints=0)
|
||||
|
||||
|
||||
async def test_range_unsupported_domain(hass):
|
||||
"""Test rangeController with unsupported domain."""
|
||||
device = ("switch.test", "on", {"friendly_name": "Test switch"})
|
||||
await discovery_test(device, hass)
|
||||
|
||||
context = Context()
|
||||
request = get_new_request("Alexa.RangeController", "SetRangeValue", "switch#test")
|
||||
request["directive"]["payload"] = {"rangeValue": "1"}
|
||||
request["directive"]["header"]["instance"] = "switch.speed"
|
||||
|
||||
msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context)
|
||||
|
||||
assert "event" in msg
|
||||
msg = msg["event"]
|
||||
assert msg["header"]["name"] == "ErrorResponse"
|
||||
assert msg["header"]["namespace"] == "Alexa"
|
||||
assert msg["payload"]["type"] == "INVALID_DIRECTIVE"
|
||||
|
||||
|
||||
async def test_mode_unsupported_domain(hass):
|
||||
"""Test modeController with unsupported domain."""
|
||||
device = ("switch.test", "on", {"friendly_name": "Test switch"})
|
||||
await discovery_test(device, hass)
|
||||
|
||||
context = Context()
|
||||
request = get_new_request("Alexa.ModeController", "SetMode", "switch#test")
|
||||
request["directive"]["payload"] = {"mode": "testMode"}
|
||||
request["directive"]["header"]["instance"] = "switch.direction"
|
||||
|
||||
msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context)
|
||||
|
||||
assert "event" in msg
|
||||
msg = msg["event"]
|
||||
assert msg["header"]["name"] == "ErrorResponse"
|
||||
assert msg["header"]["namespace"] == "Alexa"
|
||||
assert msg["payload"]["type"] == "INVALID_DIRECTIVE"
|
||||
|
|
|
@ -37,6 +37,54 @@ async def test_report_state(hass, aioclient_mock):
|
|||
assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_contact"
|
||||
|
||||
|
||||
async def test_report_state_instance(hass, aioclient_mock):
|
||||
"""Test proactive state reports with instance."""
|
||||
aioclient_mock.post(TEST_URL, text="", status=202)
|
||||
|
||||
hass.states.async_set(
|
||||
"fan.test_fan",
|
||||
"off",
|
||||
{
|
||||
"friendly_name": "Test fan",
|
||||
"supported_features": 3,
|
||||
"speed": "off",
|
||||
"oscillating": False,
|
||||
},
|
||||
)
|
||||
|
||||
await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG)
|
||||
|
||||
hass.states.async_set(
|
||||
"fan.test_fan",
|
||||
"on",
|
||||
{
|
||||
"friendly_name": "Test fan",
|
||||
"supported_features": 3,
|
||||
"speed": "high",
|
||||
"oscillating": True,
|
||||
},
|
||||
)
|
||||
|
||||
# To trigger event listener
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
call = aioclient_mock.mock_calls
|
||||
|
||||
call_json = call[0][2]
|
||||
assert call_json["event"]["header"]["namespace"] == "Alexa"
|
||||
assert call_json["event"]["header"]["name"] == "ChangeReport"
|
||||
|
||||
change_reports = call_json["event"]["payload"]["change"]["properties"]
|
||||
for report in change_reports:
|
||||
if report["name"] == "toggleState":
|
||||
assert report["value"] == "ON"
|
||||
assert report["instance"] == "fan.oscillating"
|
||||
assert report["namespace"] == "Alexa.ToggleController"
|
||||
|
||||
assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan"
|
||||
|
||||
|
||||
async def test_send_add_or_update_message(hass, aioclient_mock):
|
||||
"""Test sending an AddOrUpdateReport message."""
|
||||
aioclient_mock.post(TEST_URL, text="")
|
||||
|
|
Loading…
Add table
Reference in a new issue