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:
ochlocracy 2019-10-23 01:01:03 -04:00 committed by Paulus Schoutsen
parent dc3aa43f73
commit da094e09fa
10 changed files with 1190 additions and 53 deletions

View file

@ -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

View file

@ -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"

View file

@ -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)

View file

@ -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"

View file

@ -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()

View file

@ -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.

View file

@ -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)

View file

@ -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):

View file

@ -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"

View file

@ -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="")