core/homeassistant/components/alexa/capabilities.py

2485 lines
80 KiB
Python

"""Alexa capabilities."""
from __future__ import annotations
from collections.abc import Generator
import logging
from typing import Any
from homeassistant.components import (
button,
climate,
cover,
fan,
humidifier,
image_processing,
input_button,
input_number,
light,
media_player,
number,
remote,
timer,
vacuum,
valve,
water_heater,
)
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
CodeFormat,
)
from homeassistant.components.climate import HVACMode
from homeassistant.components.lock import LockState
from homeassistant.const import (
ATTR_CODE_FORMAT,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
STATE_IDLE,
STATE_OFF,
STATE_ON,
STATE_PAUSED,
STATE_PLAYING,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfLength,
UnitOfMass,
UnitOfTemperature,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant, State
import homeassistant.util.color as color_util
import homeassistant.util.dt as dt_util
from .const import (
API_TEMP_UNITS,
API_THERMOSTAT_MODES,
API_THERMOSTAT_PRESETS,
DATE_FORMAT,
PRESET_MODE_NA,
Inputs,
)
from .errors import UnsupportedProperty
from .resources import (
AlexaCapabilityResource,
AlexaGlobalCatalog,
AlexaModeResource,
AlexaPresetResource,
AlexaSemantics,
)
_LOGGER = logging.getLogger(__name__)
UNIT_TO_CATALOG_TAG = {
UnitOfTemperature.CELSIUS: AlexaGlobalCatalog.UNIT_TEMPERATURE_CELSIUS,
UnitOfTemperature.FAHRENHEIT: AlexaGlobalCatalog.UNIT_TEMPERATURE_FAHRENHEIT,
UnitOfTemperature.KELVIN: AlexaGlobalCatalog.UNIT_TEMPERATURE_KELVIN,
UnitOfLength.METERS: AlexaGlobalCatalog.UNIT_DISTANCE_METERS,
UnitOfLength.KILOMETERS: AlexaGlobalCatalog.UNIT_DISTANCE_KILOMETERS,
UnitOfLength.INCHES: AlexaGlobalCatalog.UNIT_DISTANCE_INCHES,
UnitOfLength.FEET: AlexaGlobalCatalog.UNIT_DISTANCE_FEET,
UnitOfLength.YARDS: AlexaGlobalCatalog.UNIT_DISTANCE_YARDS,
UnitOfLength.MILES: AlexaGlobalCatalog.UNIT_DISTANCE_MILES,
UnitOfMass.GRAMS: AlexaGlobalCatalog.UNIT_MASS_GRAMS,
UnitOfMass.KILOGRAMS: AlexaGlobalCatalog.UNIT_MASS_KILOGRAMS,
UnitOfMass.POUNDS: AlexaGlobalCatalog.UNIT_WEIGHT_POUNDS,
UnitOfMass.OUNCES: AlexaGlobalCatalog.UNIT_WEIGHT_OUNCES,
UnitOfVolume.LITERS: AlexaGlobalCatalog.UNIT_VOLUME_LITERS,
UnitOfVolume.CUBIC_FEET: AlexaGlobalCatalog.UNIT_VOLUME_CUBIC_FEET,
UnitOfVolume.CUBIC_METERS: AlexaGlobalCatalog.UNIT_VOLUME_CUBIC_METERS,
UnitOfVolume.GALLONS: AlexaGlobalCatalog.UNIT_VOLUME_GALLONS,
PERCENTAGE: AlexaGlobalCatalog.UNIT_PERCENT,
"preset": AlexaGlobalCatalog.SETTING_PRESET,
}
def get_resource_by_unit_of_measurement(entity: State) -> str:
"""Translate the unit of measurement to an Alexa Global Catalog keyword."""
unit: str = entity.attributes.get("unit_of_measurement", "preset")
return UNIT_TO_CATALOG_TAG.get(unit, AlexaGlobalCatalog.SETTING_PRESET)
class AlexaCapability:
"""Base class for Alexa capability interfaces.
The Smart Home Skills API defines a number of "capability interfaces",
roughly analogous to domains in Home Assistant. The supported interfaces
describe what actions can be performed on a particular device.
https://developer.amazon.com/docs/device-apis/message-guide.html
"""
_resource: AlexaCapabilityResource | None
_semantics: AlexaSemantics | None
supported_locales: set[str] = {"en-US"}
def __init__(
self,
entity: State,
instance: str | None = None,
non_controllable_properties: bool | None = None,
) -> None:
"""Initialize an Alexa capability."""
self.entity = entity
self.instance = instance
self._non_controllable_properties = non_controllable_properties
def name(self) -> str:
"""Return the Alexa API name of this interface."""
raise NotImplementedError
def properties_supported(self) -> list[dict]:
"""Return what properties this entity supports."""
return []
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return False
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return False
def properties_non_controllable(self) -> bool | None:
"""Return True if non controllable."""
return self._non_controllable_properties
def get_property(self, name: str) -> dict[str, Any]:
"""Read and return a property.
Return value should be a dict, or raise UnsupportedProperty.
Properties can also have a timeOfSample and uncertaintyInMilliseconds,
but returning those metadata is not yet implemented.
"""
raise UnsupportedProperty(name)
def supports_deactivation(self) -> bool | None:
"""Applicable only to scenes."""
def capability_proactively_reported(self) -> bool | None:
"""Return True if the capability is proactively reported.
Set properties_proactively_reported() for proactively reported properties.
Applicable to DoorbellEventSource.
"""
def capability_resources(self) -> dict[str, list[dict[str, Any]]]:
"""Return the capability object.
Applicable to ToggleController, RangeController, and ModeController interfaces.
"""
return {}
def configuration(self) -> dict[str, Any] | None:
"""Return the configuration object.
Applicable to the ThermostatController, SecurityControlPanel, ModeController,
RangeController, and EventDetectionSensor.
"""
def configurations(self) -> dict[str, Any] | None:
"""Return the configurations object.
The plural configurations object is different that the singular configuration
object. Applicable to EqualizerController interface.
"""
def inputs(self) -> list[dict[str, str]] | None:
"""Applicable only to media players."""
def semantics(self) -> dict[str, Any] | None:
"""Return the semantics object.
Applicable to ToggleController, RangeController, and ModeController interfaces.
"""
def supported_operations(self) -> list[str]:
"""Return the supportedOperations object."""
return []
def camera_stream_configurations(self) -> list[dict[str, Any]] | None:
"""Applicable only to CameraStreamController."""
def serialize_discovery(self) -> dict[str, Any]:
"""Serialize according to the Discovery API."""
result: dict[str, Any] = {
"type": "AlexaInterface",
"interface": self.name(),
"version": "3",
}
if (instance := self.instance) is not None:
result["instance"] = instance
if properties_supported := self.properties_supported():
result["properties"] = {
"supported": properties_supported,
"proactivelyReported": self.properties_proactively_reported(),
"retrievable": self.properties_retrievable(),
}
if (proactively_reported := self.capability_proactively_reported()) is not None:
result["proactivelyReported"] = proactively_reported
if (non_controllable := self.properties_non_controllable()) is not None:
result["properties"]["nonControllable"] = non_controllable
if (supports_deactivation := self.supports_deactivation()) is not None:
result["supportsDeactivation"] = supports_deactivation
if capability_resources := self.capability_resources():
result["capabilityResources"] = capability_resources
if configuration := self.configuration():
result["configuration"] = configuration
# The plural configurations object is different than the singular
# configuration object above.
if configurations := self.configurations():
result["configurations"] = configurations
if semantics := self.semantics():
result["semantics"] = semantics
if supported_operations := self.supported_operations():
result["supportedOperations"] = supported_operations
if inputs := self.inputs():
result["inputs"] = inputs
if camera_stream_configurations := self.camera_stream_configurations():
result["cameraStreamConfigurations"] = camera_stream_configurations
return result
def serialize_properties(self) -> Generator[dict[str, Any]]:
"""Return properties serialized for an API response."""
for prop in self.properties_supported():
prop_name = prop["name"]
try:
prop_value = self.get_property(prop_name)
except UnsupportedProperty:
raise
except Exception:
_LOGGER.exception(
"Unexpected error getting %s.%s property from %s",
self.name(),
prop_name,
self.entity,
)
prop_value = None
if prop_value is None:
continue
result = {
"name": prop_name,
"namespace": self.name(),
"value": prop_value,
"timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT),
"uncertaintyInMilliseconds": 0,
}
if (instance := self.instance) is not None:
result["instance"] = instance
yield result
class Alexa(AlexaCapability):
"""Implements Alexa Interface.
Although endpoints implement this interface implicitly,
The API suggests you should explicitly include this interface.
https://developer.amazon.com/docs/device-apis/alexa-interface.html
To compare current supported locales in Home Assistant
with Alexa supported locales, run the following script:
python -m script.alexa_locales
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa"
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
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def __init__(self, hass: HomeAssistant, entity: State) -> None:
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.EndpointHealth"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "connectivity"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "connectivity":
raise UnsupportedProperty(name)
if self.entity.state == STATE_UNAVAILABLE:
return {"value": "UNREACHABLE"}
return {"value": "OK"}
class AlexaPowerController(AlexaCapability):
"""Implements Alexa.PowerController.
https://developer.amazon.com/docs/device-apis/alexa-powercontroller.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.PowerController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "powerState"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "powerState":
raise UnsupportedProperty(name)
if self.entity.domain == climate.DOMAIN:
is_on = self.entity.state != climate.HVACMode.OFF
elif self.entity.domain == fan.DOMAIN:
is_on = self.entity.state == fan.STATE_ON
elif self.entity.domain == humidifier.DOMAIN:
is_on = self.entity.state == humidifier.STATE_ON
elif self.entity.domain == remote.DOMAIN:
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
elif self.entity.domain == vacuum.DOMAIN:
is_on = self.entity.state == vacuum.STATE_CLEANING
elif self.entity.domain == timer.DOMAIN:
is_on = self.entity.state != STATE_IDLE
elif self.entity.domain == water_heater.DOMAIN:
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
else:
is_on = self.entity.state != STATE_OFF
return "ON" if is_on else "OFF"
class AlexaLockController(AlexaCapability):
"""Implements Alexa.LockController.
https://developer.amazon.com/docs/device-apis/alexa-lockcontroller.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.LockController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "lockState"}]
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "lockState":
raise UnsupportedProperty(name)
# If its unlocking its still locked and not unlocked yet
if self.entity.state in (LockState.UNLOCKING, LockState.LOCKED):
return "LOCKED"
# If its locking its still unlocked and not locked yet
if self.entity.state in (LockState.LOCKING, LockState.UNLOCKED):
return "UNLOCKED"
return "JAMMED"
class AlexaSceneController(AlexaCapability):
"""Implements Alexa.SceneController.
https://developer.amazon.com/docs/device-apis/alexa-scenecontroller.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def __init__(self, entity: State, supports_deactivation: bool) -> None:
"""Initialize the entity."""
self._supports_deactivation = supports_deactivation
super().__init__(entity)
def supports_deactivation(self) -> bool | None:
"""Return True if the Scene controller supports deactivation."""
return self._supports_deactivation
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.SceneController"
class AlexaBrightnessController(AlexaCapability):
"""Implements Alexa.BrightnessController.
https://developer.amazon.com/docs/device-apis/alexa-brightnesscontroller.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.BrightnessController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "brightness"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "brightness":
raise UnsupportedProperty(name)
if brightness := self.entity.attributes.get("brightness"):
return round(brightness / 255.0 * 100)
return 0
class AlexaColorController(AlexaCapability):
"""Implements Alexa.ColorController.
https://developer.amazon.com/docs/device-apis/alexa-colorcontroller.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.ColorController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "color"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "color":
raise UnsupportedProperty(name)
hue_saturation: tuple[float, float] | None
if (hue_saturation := self.entity.attributes.get(light.ATTR_HS_COLOR)) is None:
hue_saturation = (0, 0)
if (brightness := self.entity.attributes.get(light.ATTR_BRIGHTNESS)) is None:
brightness = 0
return {
"hue": hue_saturation[0],
"saturation": hue_saturation[1] / 100.0,
"brightness": brightness / 255.0,
}
class AlexaColorTemperatureController(AlexaCapability):
"""Implements Alexa.ColorTemperatureController.
https://developer.amazon.com/docs/device-apis/alexa-colortemperaturecontroller.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.ColorTemperatureController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "colorTemperatureInKelvin"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "colorTemperatureInKelvin":
raise UnsupportedProperty(name)
if color_temp := self.entity.attributes.get("color_temp"):
return color_util.color_temperature_mired_to_kelvin(color_temp)
return None
class AlexaSpeaker(AlexaCapability):
"""Implements Alexa.Speaker.
https://developer.amazon.com/docs/device-apis/alexa-speaker.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"fr-FR", # Not documented as of 2021-12-04, see PR #60489
"it-IT",
"ja-JP",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.Speaker"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
properties = [{"name": "volume"}]
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & media_player.MediaPlayerEntityFeature.VOLUME_MUTE:
properties.append({"name": "muted"})
return properties
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name == "volume":
current_level = self.entity.attributes.get(
media_player.ATTR_MEDIA_VOLUME_LEVEL
)
if current_level is not None:
return round(float(current_level) * 100)
if name == "muted":
return bool(
self.entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED)
)
return None
class AlexaStepSpeaker(AlexaCapability):
"""Implements Alexa.StepSpeaker.
https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"fr-FR", # Not documented as of 2021-12-04, see PR #60489
"it-IT",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.StepSpeaker"
class AlexaPlaybackController(AlexaCapability):
"""Implements Alexa.PlaybackController.
https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.PlaybackController"
def supported_operations(self) -> list[str]:
"""Return the supportedOperations object.
Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind,
StartOver, Stop
"""
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
operations = {
media_player.MediaPlayerEntityFeature.NEXT_TRACK: "Next",
media_player.MediaPlayerEntityFeature.PAUSE: "Pause",
media_player.MediaPlayerEntityFeature.PLAY: "Play",
media_player.MediaPlayerEntityFeature.PREVIOUS_TRACK: "Previous",
media_player.MediaPlayerEntityFeature.STOP: "Stop",
}
return [
value
for operation, value in operations.items()
if operation & supported_features
]
class AlexaInputController(AlexaCapability):
"""Implements Alexa.InputController.
https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.InputController"
def inputs(self) -> list[dict[str, str]] | None:
"""Return the list of valid supported inputs."""
source_list: list[Any] = (
self.entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or []
)
return AlexaInputController.get_valid_inputs(source_list)
@staticmethod
def get_valid_inputs(source_list: list[Any]) -> list[dict[str, str]]:
"""Return list of supported inputs."""
input_list: list[dict[str, str]] = []
for source in source_list:
if not isinstance(source, str):
continue
formatted_source = (
source.lower().replace("-", "").replace("_", "").replace(" ", "")
)
if formatted_source in Inputs.VALID_SOURCE_NAME_MAP:
input_list.append(
{"name": Inputs.VALID_SOURCE_NAME_MAP[formatted_source]}
)
return input_list
class AlexaTemperatureSensor(AlexaCapability):
"""Implements Alexa.TemperatureSensor.
https://developer.amazon.com/docs/device-apis/alexa-temperaturesensor.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def __init__(self, hass: HomeAssistant, entity: State) -> None:
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.TemperatureSensor"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "temperature"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "temperature":
raise UnsupportedProperty(name)
unit: str = self.entity.attributes.get(
ATTR_UNIT_OF_MEASUREMENT, self.hass.config.units.temperature_unit
)
temp: str | None = self.entity.state
if self.entity.domain == climate.DOMAIN:
unit = self.hass.config.units.temperature_unit
temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)
elif self.entity.domain == water_heater.DOMAIN:
unit = self.hass.config.units.temperature_unit
temp = self.entity.attributes.get(water_heater.ATTR_CURRENT_TEMPERATURE)
if temp is None or temp in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return None
try:
temp_float = float(temp)
except ValueError:
_LOGGER.warning("Invalid temp value %s for %s", temp, self.entity.entity_id)
return None
# Alexa displays temperatures with one decimal digit, we don't need to do
# rounding for presentation here.
return {"value": temp_float, "scale": API_TEMP_UNITS[UnitOfTemperature(unit)]}
class AlexaContactSensor(AlexaCapability):
"""Implements Alexa.ContactSensor.
The Alexa.ContactSensor interface describes the properties and events used
to report the state of an endpoint that detects contact between two
surfaces. For example, a contact sensor can report whether a door or window
is open.
https://developer.amazon.com/docs/device-apis/alexa-contactsensor.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def __init__(self, hass: HomeAssistant, entity: State) -> None:
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.ContactSensor"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "detectionState"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "detectionState":
raise UnsupportedProperty(name)
if self.entity.state == STATE_ON:
return "DETECTED"
return "NOT_DETECTED"
class AlexaMotionSensor(AlexaCapability):
"""Implements Alexa.MotionSensor.
https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def __init__(self, hass: HomeAssistant, entity: State) -> None:
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.MotionSensor"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "detectionState"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "detectionState":
raise UnsupportedProperty(name)
if self.entity.state == STATE_ON:
return "DETECTED"
return "NOT_DETECTED"
class AlexaThermostatController(AlexaCapability):
"""Implements Alexa.ThermostatController.
https://developer.amazon.com/docs/device-apis/alexa-thermostatcontroller.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def __init__(self, hass: HomeAssistant, entity: State) -> None:
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.ThermostatController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
properties = [{"name": "thermostatMode"}]
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if self.entity.domain == climate.DOMAIN:
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
properties.append({"name": "lowerSetpoint"})
properties.append({"name": "upperSetpoint"})
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE:
properties.append({"name": "targetSetpoint"})
elif (
self.entity.domain == water_heater.DOMAIN
and supported & water_heater.WaterHeaterEntityFeature.TARGET_TEMPERATURE
):
properties.append({"name": "targetSetpoint"})
return properties
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if self.entity.state == STATE_UNAVAILABLE:
return None
if name == "thermostatMode":
if self.entity.domain == water_heater.DOMAIN:
return None
preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE)
mode: dict[str, str] | str | None
if preset in API_THERMOSTAT_PRESETS:
mode = API_THERMOSTAT_PRESETS[preset]
elif self.entity.state == STATE_UNKNOWN:
return None
else:
if self.entity.state not in API_THERMOSTAT_MODES:
_LOGGER.error(
"%s (%s) has unsupported state value '%s'",
self.entity.entity_id,
type(self.entity),
self.entity.state,
)
raise UnsupportedProperty(name)
mode = API_THERMOSTAT_MODES[HVACMode(self.entity.state)]
return mode
unit = self.hass.config.units.temperature_unit
if name == "targetSetpoint":
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
elif name == "lowerSetpoint":
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
elif name == "upperSetpoint":
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
else:
raise UnsupportedProperty(name)
if temp is None:
return None
try:
temp = float(temp)
except ValueError:
_LOGGER.warning(
"Invalid temp value %s for %s in %s", temp, name, self.entity.entity_id
)
return None
return {"value": temp, "scale": API_TEMP_UNITS[unit]}
def configuration(self) -> dict[str, Any] | None:
"""Return configuration object.
Translates climate HVAC_MODES and PRESETS to supported Alexa
ThermostatMode Values.
ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM.
Water heater devices do not return thermostat modes.
"""
if self.entity.domain == water_heater.DOMAIN:
return None
hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES) or []
supported_modes: list[str] = [
API_THERMOSTAT_MODES[mode]
for mode in hvac_modes
if mode in API_THERMOSTAT_MODES
]
preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES)
if preset_modes:
for mode in preset_modes:
thermostat_mode = API_THERMOSTAT_PRESETS.get(mode)
if thermostat_mode:
supported_modes.append(thermostat_mode)
# Return False for supportsScheduling until supported with event
# listener in handler.
configuration: dict[str, Any] = {"supportsScheduling": False}
if supported_modes:
configuration["supportedModes"] = supported_modes
return configuration
class AlexaPowerLevelController(AlexaCapability):
"""Implements Alexa.PowerLevelController.
https://developer.amazon.com/docs/device-apis/alexa-powerlevelcontroller.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"fr-CA",
"fr-FR",
"it-IT",
"ja-JP",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.PowerLevelController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "powerLevel"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "powerLevel":
raise UnsupportedProperty(name)
class AlexaSecurityPanelController(AlexaCapability):
"""Implements Alexa.SecurityPanelController.
https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"it-IT",
"ja-JP",
"pt-BR",
}
def __init__(self, hass: HomeAssistant, entity: State) -> None:
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.SecurityPanelController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "armState"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "armState":
raise UnsupportedProperty(name)
arm_state = self.entity.state
if arm_state == AlarmControlPanelState.ARMED_HOME:
return "ARMED_STAY"
if arm_state == AlarmControlPanelState.ARMED_AWAY:
return "ARMED_AWAY"
if arm_state == AlarmControlPanelState.ARMED_NIGHT:
return "ARMED_NIGHT"
if arm_state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS:
return "ARMED_STAY"
return "DISARMED"
def configuration(self) -> dict[str, Any] | None:
"""Return configuration object with supported authorization types."""
code_format = self.entity.attributes.get(ATTR_CODE_FORMAT)
supported = self.entity.attributes[ATTR_SUPPORTED_FEATURES]
configuration = {}
supported_arm_states = [{"value": "DISARMED"}]
if supported & AlarmControlPanelEntityFeature.ARM_AWAY:
supported_arm_states.append({"value": "ARMED_AWAY"})
if supported & AlarmControlPanelEntityFeature.ARM_HOME:
supported_arm_states.append({"value": "ARMED_STAY"})
if supported & AlarmControlPanelEntityFeature.ARM_NIGHT:
supported_arm_states.append({"value": "ARMED_NIGHT"})
configuration["supportedArmStates"] = supported_arm_states
if code_format == CodeFormat.NUMBER:
configuration["supportedAuthorizationTypes"] = [{"type": "FOUR_DIGIT_PIN"}]
return configuration
class AlexaModeController(AlexaCapability):
"""Implements Alexa.ModeController.
The instance property must be unique across ModeController, RangeController,
ToggleController within the same device.
The instance property should be a concatenated string of device domain period
and single word. e.g. fan.speed & fan.direction.
The instance property must not contain words from other instance property
strings within the same device. e.g. Instance property cover.position &
cover.tilt_position will cause the Alexa.Discovery directive to fail.
An instance property string value may be reused for different devices.
https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def __init__(
self, entity: State, instance: str, non_controllable: bool = False
) -> None:
"""Initialize the entity."""
AlexaCapability.__init__(self, entity, instance, non_controllable)
self._resource = None
self._semantics = None
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.ModeController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "mode"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "mode":
raise UnsupportedProperty(name)
# Fan Direction
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
mode = self.entity.attributes.get(fan.ATTR_DIRECTION, None)
if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN):
return f"{fan.ATTR_DIRECTION}.{mode}"
# Fan preset_mode
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None)
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None):
return f"{fan.ATTR_PRESET_MODE}.{mode}"
# Humidifier mode
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}":
mode = self.entity.attributes.get(humidifier.ATTR_MODE)
modes: list[str] = (
self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES) or []
)
if mode in modes:
return f"{humidifier.ATTR_MODE}.{mode}"
# Remote Activity
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
activity = self.entity.attributes.get(remote.ATTR_CURRENT_ACTIVITY, None)
if activity in self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST, []):
return f"{remote.ATTR_ACTIVITY}.{activity}"
# Water heater operation mode
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
operation_mode = self.entity.attributes.get(
water_heater.ATTR_OPERATION_MODE
)
operation_modes: list[str] = (
self.entity.attributes.get(water_heater.ATTR_OPERATION_LIST) or []
)
if operation_mode in operation_modes:
return f"{water_heater.ATTR_OPERATION_MODE}.{operation_mode}"
# Cover Position
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
# Return state instead of position when using ModeController.
mode = self.entity.state
if mode in (
cover.STATE_OPEN,
cover.STATE_OPENING,
cover.STATE_CLOSED,
cover.STATE_CLOSING,
STATE_UNKNOWN,
):
return f"{cover.ATTR_POSITION}.{mode}"
# Valve position state
if self.instance == f"{valve.DOMAIN}.state":
# Return state instead of position when using ModeController.
state = self.entity.state
if state in (
valve.STATE_OPEN,
valve.STATE_OPENING,
valve.STATE_CLOSED,
valve.STATE_CLOSING,
STATE_UNKNOWN,
):
return f"state.{state}"
return None
def configuration(self) -> dict[str, Any] | None:
"""Return configuration with modeResources."""
if isinstance(self._resource, AlexaCapabilityResource):
return self._resource.serialize_configuration()
return None
def capability_resources(self) -> dict[str, list[dict[str, Any]]]:
"""Return capabilityResources object."""
# Fan Direction Resource
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
self._resource = AlexaModeResource(
[AlexaGlobalCatalog.SETTING_DIRECTION], False
)
self._resource.add_mode(
f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", [fan.DIRECTION_FORWARD]
)
self._resource.add_mode(
f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", [fan.DIRECTION_REVERSE]
)
return self._resource.serialize_capability_resources()
# Fan preset_mode
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
self._resource = AlexaModeResource(
[AlexaGlobalCatalog.SETTING_PRESET], False
)
preset_modes = self.entity.attributes.get(fan.ATTR_PRESET_MODES) or []
for preset_mode in preset_modes:
self._resource.add_mode(
f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_mode]
)
# Fans with a single preset_mode completely break Alexa discovery, add a
# fake preset (see issue #53832).
if len(preset_modes) == 1:
self._resource.add_mode(
f"{fan.ATTR_PRESET_MODE}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
)
return self._resource.serialize_capability_resources()
# Humidifier modes
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}":
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
modes = self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES) or []
for mode in modes:
self._resource.add_mode(f"{humidifier.ATTR_MODE}.{mode}", [mode])
# Humidifiers or Fans with a single mode completely break Alexa discovery,
# add a fake preset (see issue #53832).
if len(modes) == 1:
self._resource.add_mode(
f"{humidifier.ATTR_MODE}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
)
return self._resource.serialize_capability_resources()
# Water heater operation modes
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
operation_modes = (
self.entity.attributes.get(water_heater.ATTR_OPERATION_LIST) or []
)
for operation_mode in operation_modes:
self._resource.add_mode(
f"{water_heater.ATTR_OPERATION_MODE}.{operation_mode}",
[operation_mode],
)
# Devices with a single mode completely break Alexa discovery,
# add a fake preset (see issue #53832).
if len(operation_modes) == 1:
self._resource.add_mode(
f"{water_heater.ATTR_OPERATION_MODE}.{PRESET_MODE_NA}",
[PRESET_MODE_NA],
)
return self._resource.serialize_capability_resources()
# Remote Resource
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
# Use the mode controller for a remote because the input controller
# only allows a preset of names as an input.
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
for activity in activities:
self._resource.add_mode(
f"{remote.ATTR_ACTIVITY}.{activity}", [activity]
)
# Remotes with a single activity completely break Alexa discovery, add a
# fake activity to the mode controller (see issue #53832).
if len(activities) == 1:
self._resource.add_mode(
f"{remote.ATTR_ACTIVITY}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
)
return self._resource.serialize_capability_resources()
# Cover Position Resources
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
self._resource = AlexaModeResource(
["Position", AlexaGlobalCatalog.SETTING_OPENING], False
)
self._resource.add_mode(
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
[AlexaGlobalCatalog.VALUE_OPEN],
)
self._resource.add_mode(
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
[AlexaGlobalCatalog.VALUE_CLOSE],
)
self._resource.add_mode(
f"{cover.ATTR_POSITION}.custom",
["Custom", AlexaGlobalCatalog.SETTING_PRESET],
)
return self._resource.serialize_capability_resources()
# Valve position resources
if self.instance == f"{valve.DOMAIN}.state":
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
self._resource = AlexaModeResource(
["Preset", AlexaGlobalCatalog.SETTING_PRESET], False
)
modes = 0
if supported_features & valve.ValveEntityFeature.OPEN:
self._resource.add_mode(
f"state.{valve.STATE_OPEN}",
["Open", AlexaGlobalCatalog.SETTING_PRESET],
)
modes += 1
if supported_features & valve.ValveEntityFeature.CLOSE:
self._resource.add_mode(
f"state.{valve.STATE_CLOSED}",
["Closed", AlexaGlobalCatalog.SETTING_PRESET],
)
modes += 1
# Alexa requires at least 2 modes
if modes == 1:
self._resource.add_mode(f"state.{PRESET_MODE_NA}", [PRESET_MODE_NA])
return self._resource.serialize_capability_resources()
return {}
def semantics(self) -> dict[str, Any] | None:
"""Build and return semantics object."""
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
# Cover Position
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
lower_labels = [AlexaSemantics.ACTION_LOWER]
raise_labels = [AlexaSemantics.ACTION_RAISE]
self._semantics = AlexaSemantics()
# Add open/close semantics if tilt is not supported.
if not supported & cover.CoverEntityFeature.SET_TILT_POSITION:
lower_labels.append(AlexaSemantics.ACTION_CLOSE)
raise_labels.append(AlexaSemantics.ACTION_OPEN)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_CLOSED],
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_OPEN],
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
)
self._semantics.add_action_to_directive(
lower_labels,
"SetMode",
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
)
self._semantics.add_action_to_directive(
raise_labels,
"SetMode",
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"},
)
return self._semantics.serialize_semantics()
# Valve Position
if self.instance == f"{valve.DOMAIN}.state":
close_labels = [AlexaSemantics.ACTION_CLOSE]
open_labels = [AlexaSemantics.ACTION_OPEN]
self._semantics = AlexaSemantics()
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_CLOSED],
f"state.{valve.STATE_CLOSED}",
)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_OPEN],
f"state.{valve.STATE_OPEN}",
)
self._semantics.add_action_to_directive(
close_labels,
"SetMode",
{"mode": f"state.{valve.STATE_CLOSED}"},
)
self._semantics.add_action_to_directive(
open_labels,
"SetMode",
{"mode": f"state.{valve.STATE_OPEN}"},
)
return self._semantics.serialize_semantics()
return None
class AlexaRangeController(AlexaCapability):
"""Implements Alexa.RangeController.
The instance property must be unique across ModeController, RangeController,
ToggleController within the same device.
The instance property should be a concatenated string of device domain period
and single word. e.g. fan.speed & fan.direction.
The instance property must not contain words from other instance property
strings within the same device. e.g. Instance property cover.position &
cover.tilt_position will cause the Alexa.Discovery directive to fail.
An instance property string value may be reused for different devices.
https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def __init__(
self, entity: State, instance: str | None, non_controllable: bool = False
) -> None:
"""Initialize the entity."""
AlexaCapability.__init__(self, entity, instance, non_controllable)
self._resource = None
self._semantics = None
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.RangeController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "rangeValue"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "rangeValue":
raise UnsupportedProperty(name)
# Return None for unavailable and unknown states.
# Allows the Alexa.EndpointHealth Interface to handle the unavailable
# state in a stateReport.
if self.entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None):
return None
# Cover Position
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION)
# Cover Tilt
if self.instance == f"{cover.DOMAIN}.tilt":
return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION)
# Fan speed percentage
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported and fan.FanEntityFeature.SET_SPEED:
return self.entity.attributes.get(fan.ATTR_PERCENTAGE)
return 100 if self.entity.state == fan.STATE_ON else 0
# Humidifier target humidity
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}":
# If the humidifier is turned off the target humidity attribute is not set.
# We return 0 to make clear we do not know the current value.
return self.entity.attributes.get(humidifier.ATTR_HUMIDITY, 0)
# Input Number Value
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
return float(self.entity.state)
# Number Value
if self.instance == f"{number.DOMAIN}.{number.ATTR_VALUE}":
return float(self.entity.state)
# Vacuum Fan Speed
if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}":
speed_list = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED_LIST)
speed = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED)
if speed_list is not None and speed is not None:
return next((i for i, v in enumerate(speed_list) if v == speed), None)
# Valve Position
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
return self.entity.attributes.get(valve.ATTR_CURRENT_POSITION)
return None
def configuration(self) -> dict[str, Any] | None:
"""Return configuration with presetResources."""
if isinstance(self._resource, AlexaCapabilityResource):
return self._resource.serialize_configuration()
return None
def capability_resources(self) -> dict[str, list[dict[str, Any]]]:
"""Return capabilityResources object."""
# Fan Speed Percentage Resources
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
percentage_step = self.entity.attributes.get(fan.ATTR_PERCENTAGE_STEP)
self._resource = AlexaPresetResource(
labels=["Percentage", AlexaGlobalCatalog.SETTING_FAN_SPEED],
min_value=0,
max_value=100,
# precision must be a divider of 100 and must be an integer; set step
# size to 1 for a consistent behavior except for on/off fans
precision=1 if percentage_step else 100,
unit=AlexaGlobalCatalog.UNIT_PERCENT,
)
return self._resource.serialize_capability_resources()
# Humidifier Target Humidity Resources
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}":
self._resource = AlexaPresetResource(
labels=["Humidity", "Percentage", "Target humidity"],
min_value=self.entity.attributes.get(humidifier.ATTR_MIN_HUMIDITY, 10),
max_value=self.entity.attributes.get(humidifier.ATTR_MAX_HUMIDITY, 90),
precision=1,
unit=AlexaGlobalCatalog.UNIT_PERCENT,
)
return self._resource.serialize_capability_resources()
# Cover Position Resources
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
self._resource = AlexaPresetResource(
["Position", AlexaGlobalCatalog.SETTING_OPENING],
min_value=0,
max_value=100,
precision=1,
unit=AlexaGlobalCatalog.UNIT_PERCENT,
)
return self._resource.serialize_capability_resources()
# Cover Tilt Resources
if self.instance == f"{cover.DOMAIN}.tilt":
self._resource = AlexaPresetResource(
["Tilt", "Angle", AlexaGlobalCatalog.SETTING_DIRECTION],
min_value=0,
max_value=100,
precision=1,
unit=AlexaGlobalCatalog.UNIT_PERCENT,
)
return self._resource.serialize_capability_resources()
# Input Number Value
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
min_value = float(self.entity.attributes[input_number.ATTR_MIN])
max_value = float(self.entity.attributes[input_number.ATTR_MAX])
precision = float(self.entity.attributes.get(input_number.ATTR_STEP, 1))
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._resource = AlexaPresetResource(
["Value", get_resource_by_unit_of_measurement(self.entity)],
min_value=min_value,
max_value=max_value,
precision=precision,
unit=unit,
)
self._resource.add_preset(
value=min_value, labels=[AlexaGlobalCatalog.VALUE_MINIMUM]
)
self._resource.add_preset(
value=max_value, labels=[AlexaGlobalCatalog.VALUE_MAXIMUM]
)
return self._resource.serialize_capability_resources()
# Number Value
if self.instance == f"{number.DOMAIN}.{number.ATTR_VALUE}":
min_value = float(self.entity.attributes[number.ATTR_MIN])
max_value = float(self.entity.attributes[number.ATTR_MAX])
precision = float(self.entity.attributes.get(number.ATTR_STEP, 1))
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._resource = AlexaPresetResource(
["Value", get_resource_by_unit_of_measurement(self.entity)],
min_value=min_value,
max_value=max_value,
precision=precision,
unit=unit,
)
self._resource.add_preset(
value=min_value, labels=[AlexaGlobalCatalog.VALUE_MINIMUM]
)
self._resource.add_preset(
value=max_value, labels=[AlexaGlobalCatalog.VALUE_MAXIMUM]
)
return self._resource.serialize_capability_resources()
# Vacuum Fan Speed Resources
if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}":
speed_list = self.entity.attributes[vacuum.ATTR_FAN_SPEED_LIST]
max_value = len(speed_list) - 1
self._resource = AlexaPresetResource(
labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED],
min_value=0,
max_value=max_value,
precision=1,
)
for index, speed in enumerate(speed_list):
labels = [speed.replace("_", " ")]
if index == 1:
labels.append(AlexaGlobalCatalog.VALUE_MINIMUM)
if index == max_value:
labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM)
self._resource.add_preset(value=index, labels=labels)
return self._resource.serialize_capability_resources()
# Valve Position Resources
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
self._resource = AlexaPresetResource(
["Opening", AlexaGlobalCatalog.SETTING_OPENING],
min_value=0,
max_value=100,
precision=1,
unit=AlexaGlobalCatalog.UNIT_PERCENT,
)
return self._resource.serialize_capability_resources()
return {}
def semantics(self) -> dict[str, Any] | None:
"""Build and return semantics object."""
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
# Cover Position
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
lower_labels = [AlexaSemantics.ACTION_LOWER]
raise_labels = [AlexaSemantics.ACTION_RAISE]
self._semantics = AlexaSemantics()
# Add open/close semantics if tilt is not supported.
if not supported & cover.CoverEntityFeature.SET_TILT_POSITION:
lower_labels.append(AlexaSemantics.ACTION_CLOSE)
raise_labels.append(AlexaSemantics.ACTION_OPEN)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_CLOSED], value=0
)
self._semantics.add_states_to_range(
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
)
self._semantics.add_action_to_directive(
lower_labels, "SetRangeValue", {"rangeValue": 0}
)
self._semantics.add_action_to_directive(
raise_labels, "SetRangeValue", {"rangeValue": 100}
)
return self._semantics.serialize_semantics()
# Cover Tilt
if self.instance == f"{cover.DOMAIN}.tilt":
self._semantics = AlexaSemantics()
self._semantics.add_action_to_directive(
[AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0}
)
self._semantics.add_action_to_directive(
[AlexaSemantics.ACTION_OPEN], "SetRangeValue", {"rangeValue": 100}
)
self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0)
self._semantics.add_states_to_range(
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
)
return self._semantics.serialize_semantics()
# Fan Speed Percentage
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
lower_labels = [AlexaSemantics.ACTION_LOWER]
raise_labels = [AlexaSemantics.ACTION_RAISE]
self._semantics = AlexaSemantics()
self._semantics.add_action_to_directive(
lower_labels, "SetRangeValue", {"rangeValue": 0}
)
self._semantics.add_action_to_directive(
raise_labels, "SetRangeValue", {"rangeValue": 100}
)
return self._semantics.serialize_semantics()
# Target Humidity Percentage
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}":
lower_labels = [AlexaSemantics.ACTION_LOWER]
raise_labels = [AlexaSemantics.ACTION_RAISE]
self._semantics = AlexaSemantics()
min_value = self.entity.attributes.get(humidifier.ATTR_MIN_HUMIDITY, 10)
max_value = self.entity.attributes.get(humidifier.ATTR_MAX_HUMIDITY, 90)
self._semantics.add_action_to_directive(
lower_labels, "SetRangeValue", {"rangeValue": min_value}
)
self._semantics.add_action_to_directive(
raise_labels, "SetRangeValue", {"rangeValue": max_value}
)
return self._semantics.serialize_semantics()
# Valve Position
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
close_labels = [AlexaSemantics.ACTION_CLOSE]
open_labels = [AlexaSemantics.ACTION_OPEN]
self._semantics = AlexaSemantics()
self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0)
self._semantics.add_states_to_range(
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
)
self._semantics.add_action_to_directive(
close_labels, "SetRangeValue", {"rangeValue": 0}
)
self._semantics.add_action_to_directive(
open_labels, "SetRangeValue", {"rangeValue": 100}
)
return self._semantics.serialize_semantics()
return None
class AlexaToggleController(AlexaCapability):
"""Implements Alexa.ToggleController.
The instance property must be unique across ModeController, RangeController,
ToggleController within the same device.
The instance property should be a concatenated string of device domain period
and single word. e.g. fan.speed & fan.direction.
The instance property must not contain words from other instance property
strings within the same device. e.g. Instance property cover.position
& cover.tilt_position will cause the Alexa.Discovery directive to fail.
An instance property string value may be reused for different devices.
https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def __init__(
self, entity: State, instance: str, non_controllable: bool = False
) -> None:
"""Initialize the entity."""
AlexaCapability.__init__(self, entity, instance, non_controllable)
self._resource = None
self._semantics = None
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.ToggleController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "toggleState"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "toggleState":
raise UnsupportedProperty(name)
# Fan Oscillating
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"
# Stop Valve
if self.instance == f"{valve.DOMAIN}.stop":
return "OFF"
return None
def capability_resources(self) -> dict[str, list[dict[str, Any]]]:
"""Return capabilityResources object."""
# Fan Oscillating Resource
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
self._resource = AlexaCapabilityResource(
[AlexaGlobalCatalog.SETTING_OSCILLATE, "Rotate", "Rotation"]
)
return self._resource.serialize_capability_resources()
if self.instance == f"{valve.DOMAIN}.stop":
self._resource = AlexaCapabilityResource(["Stop"])
return self._resource.serialize_capability_resources()
return {}
class AlexaChannelController(AlexaCapability):
"""Implements Alexa.ChannelController.
https://developer.amazon.com/docs/device-apis/alexa-channelcontroller.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.ChannelController"
class AlexaDoorbellEventSource(AlexaCapability):
"""Implements Alexa.DoorbellEventSource.
https://developer.amazon.com/docs/device-apis/alexa-doorbelleventsource.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.DoorbellEventSource"
def capability_proactively_reported(self) -> bool:
"""Return True for proactively reported capability."""
return True
class AlexaPlaybackStateReporter(AlexaCapability):
"""Implements Alexa.PlaybackStateReporter.
https://developer.amazon.com/docs/device-apis/alexa-playbackstatereporter.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.PlaybackStateReporter"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "playbackState"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "playbackState":
raise UnsupportedProperty(name)
playback_state = self.entity.state
if playback_state == STATE_PLAYING:
return {"state": "PLAYING"}
if playback_state == STATE_PAUSED:
return {"state": "PAUSED"}
return {"state": "STOPPED"}
class AlexaSeekController(AlexaCapability):
"""Implements Alexa.SeekController.
https://developer.amazon.com/docs/device-apis/alexa-seekcontroller.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.SeekController"
class AlexaEventDetectionSensor(AlexaCapability):
"""Implements Alexa.EventDetectionSensor.
https://developer.amazon.com/docs/device-apis/alexa-eventdetectionsensor.html
"""
supported_locales = {"en-US"}
def __init__(self, hass: HomeAssistant, entity: State) -> None:
"""Initialize the entity."""
super().__init__(entity)
self.hass = hass
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.EventDetectionSensor"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports."""
return [{"name": "humanPresenceDetectionState"}]
def properties_proactively_reported(self) -> bool:
"""Return True if properties asynchronously reported."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "humanPresenceDetectionState":
raise UnsupportedProperty(name)
human_presence = "NOT_DETECTED"
state = self.entity.state
# Return None for unavailable and unknown states.
# Allows the Alexa.EndpointHealth Interface to handle the unavailable
# state in a stateReport.
if state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None):
return None
if self.entity.domain == image_processing.DOMAIN:
if int(state):
human_presence = "DETECTED"
elif state == STATE_ON or self.entity.domain in [
input_button.DOMAIN,
button.DOMAIN,
]:
human_presence = "DETECTED"
return {"value": human_presence}
def configuration(self) -> dict[str, Any] | None:
"""Return supported detection types."""
return {
"detectionMethods": ["AUDIO", "VIDEO"],
"detectionModes": {
"humanPresence": {
"featureAvailability": "ENABLED",
"supportsNotDetected": self.entity.domain
not in [input_button.DOMAIN, button.DOMAIN],
}
},
}
class AlexaEqualizerController(AlexaCapability):
"""Implements Alexa.EqualizerController.
https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-equalizercontroller.html
"""
supported_locales = {
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
VALID_SOUND_MODES = {
"MOVIE",
"MUSIC",
"NIGHT",
"SPORT",
"TV",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.EqualizerController"
def properties_supported(self) -> list[dict[str, str]]:
"""Return what properties this entity supports.
Either bands, mode or both can be specified. Only mode is supported
at this time.
"""
return [{"name": "mode"}]
def properties_retrievable(self) -> bool:
"""Return True if properties can be retrieved."""
return True
def get_property(self, name: str) -> Any:
"""Read and return a property."""
if name != "mode":
raise UnsupportedProperty(name)
sound_mode = self.entity.attributes.get(media_player.ATTR_SOUND_MODE)
if sound_mode and sound_mode.upper() in self.VALID_SOUND_MODES:
return sound_mode.upper()
return None
def configurations(self) -> dict[str, Any] | None:
"""Return the sound modes supported in the configurations object."""
configurations = None
supported_sound_modes = self.get_valid_inputs(
self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) or []
)
if supported_sound_modes:
configurations = {"modes": {"supported": supported_sound_modes}}
return configurations
@classmethod
def get_valid_inputs(cls, sound_mode_list: list[str]) -> list[dict[str, str]]:
"""Return list of supported inputs."""
input_list: list[dict[str, str]] = []
for sound_mode in sound_mode_list:
sound_mode = sound_mode.upper()
if sound_mode in cls.VALID_SOUND_MODES:
input_list.append({"name": sound_mode})
return input_list
class AlexaTimeHoldController(AlexaCapability):
"""Implements Alexa.TimeHoldController.
https://developer.amazon.com/docs/device-apis/alexa-timeholdcontroller.html
"""
supported_locales = {"en-US"}
def __init__(self, entity: State, allow_remote_resume: bool = False) -> None:
"""Initialize the entity."""
super().__init__(entity)
self._allow_remote_resume = allow_remote_resume
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.TimeHoldController"
def configuration(self) -> dict[str, Any] | None:
"""Return configuration object.
Set allowRemoteResume to True if Alexa can restart the operation on the device.
When false, Alexa does not send the Resume directive.
"""
return {"allowRemoteResume": self._allow_remote_resume}
class AlexaCameraStreamController(AlexaCapability):
"""Implements Alexa.CameraStreamController.
https://developer.amazon.com/docs/device-apis/alexa-camerastreamcontroller.html
"""
supported_locales = {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}
def name(self) -> str:
"""Return the Alexa API name of this interface."""
return "Alexa.CameraStreamController"
def camera_stream_configurations(self) -> list[dict[str, Any]] | None:
"""Return cameraStreamConfigurations object."""
return [
{
"protocols": ["HLS"],
"resolutions": [{"width": 1280, "height": 720}],
"authorizationTypes": ["NONE"],
"videoCodecs": ["H264"],
"audioCodecs": ["AAC"],
}
]