598 lines
18 KiB
Python
598 lines
18 KiB
Python
"""Alexa capabilities."""
|
|
from datetime import datetime
|
|
import logging
|
|
|
|
from homeassistant.const import (
|
|
ATTR_SUPPORTED_FEATURES,
|
|
ATTR_TEMPERATURE,
|
|
ATTR_UNIT_OF_MEASUREMENT,
|
|
STATE_LOCKED,
|
|
STATE_OFF,
|
|
STATE_ON,
|
|
STATE_UNAVAILABLE,
|
|
STATE_UNLOCKED,
|
|
)
|
|
import homeassistant.components.climate.const as climate
|
|
from homeassistant.components import (
|
|
light,
|
|
fan,
|
|
cover,
|
|
)
|
|
import homeassistant.util.color as color_util
|
|
|
|
from .const import (
|
|
API_TEMP_UNITS,
|
|
API_THERMOSTAT_MODES,
|
|
DATE_FORMAT,
|
|
PERCENTAGE_FAN_MAP,
|
|
)
|
|
from .errors import UnsupportedProperty
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class AlexaCapibility:
|
|
"""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
|
|
"""
|
|
|
|
def __init__(self, entity):
|
|
"""Initialize an Alexa capibility."""
|
|
self.entity = entity
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
raise NotImplementedError
|
|
|
|
@staticmethod
|
|
def properties_supported():
|
|
"""Return what properties this entity supports."""
|
|
return []
|
|
|
|
@staticmethod
|
|
def properties_proactively_reported():
|
|
"""Return True if properties asynchronously reported."""
|
|
return False
|
|
|
|
@staticmethod
|
|
def properties_retrievable():
|
|
"""Return True if properties can be retrieved."""
|
|
return False
|
|
|
|
@staticmethod
|
|
def get_property(name):
|
|
"""Read and return a property.
|
|
|
|
Return value should be a dict, or raise UnsupportedProperty.
|
|
|
|
Properties can also have a timeOfSample and uncertaintyInMilliseconds,
|
|
but returning those metadata is not yet implemented.
|
|
"""
|
|
raise UnsupportedProperty(name)
|
|
|
|
@staticmethod
|
|
def supports_deactivation():
|
|
"""Applicable only to scenes."""
|
|
return None
|
|
|
|
def serialize_discovery(self):
|
|
"""Serialize according to the Discovery API."""
|
|
result = {
|
|
'type': 'AlexaInterface',
|
|
'interface': self.name(),
|
|
'version': '3',
|
|
'properties': {
|
|
'supported': self.properties_supported(),
|
|
'proactivelyReported': self.properties_proactively_reported(),
|
|
'retrievable': self.properties_retrievable(),
|
|
},
|
|
}
|
|
|
|
# pylint: disable=assignment-from-none
|
|
supports_deactivation = self.supports_deactivation()
|
|
if supports_deactivation is not None:
|
|
result['supportsDeactivation'] = supports_deactivation
|
|
return result
|
|
|
|
def serialize_properties(self):
|
|
"""Return properties serialized for an API response."""
|
|
for prop in self.properties_supported():
|
|
prop_name = prop['name']
|
|
# pylint: disable=assignment-from-no-return
|
|
prop_value = self.get_property(prop_name)
|
|
if prop_value is not None:
|
|
yield {
|
|
'name': prop_name,
|
|
'namespace': self.name(),
|
|
'value': prop_value,
|
|
'timeOfSample': datetime.now().strftime(DATE_FORMAT),
|
|
'uncertaintyInMilliseconds': 0
|
|
}
|
|
|
|
|
|
class AlexaEndpointHealth(AlexaCapibility):
|
|
"""Implements Alexa.EndpointHealth.
|
|
|
|
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it
|
|
"""
|
|
|
|
def __init__(self, hass, entity):
|
|
"""Initialize the entity."""
|
|
super().__init__(entity)
|
|
self.hass = hass
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.EndpointHealth'
|
|
|
|
def properties_supported(self):
|
|
"""Return what properties this entity supports."""
|
|
return [{'name': 'connectivity'}]
|
|
|
|
def properties_proactively_reported(self):
|
|
"""Return True if properties asynchronously reported."""
|
|
return False
|
|
|
|
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 != 'connectivity':
|
|
raise UnsupportedProperty(name)
|
|
|
|
if self.entity.state == STATE_UNAVAILABLE:
|
|
return {'value': 'UNREACHABLE'}
|
|
return {'value': 'OK'}
|
|
|
|
|
|
class AlexaPowerController(AlexaCapibility):
|
|
"""Implements Alexa.PowerController.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-powercontroller.html
|
|
"""
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.PowerController'
|
|
|
|
def properties_supported(self):
|
|
"""Return what properties this entity supports."""
|
|
return [{'name': 'powerState'}]
|
|
|
|
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 != 'powerState':
|
|
raise UnsupportedProperty(name)
|
|
|
|
if self.entity.state == STATE_OFF:
|
|
return 'OFF'
|
|
return 'ON'
|
|
|
|
|
|
class AlexaLockController(AlexaCapibility):
|
|
"""Implements Alexa.LockController.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-lockcontroller.html
|
|
"""
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.LockController'
|
|
|
|
def properties_supported(self):
|
|
"""Return what properties this entity supports."""
|
|
return [{'name': 'lockState'}]
|
|
|
|
def properties_retrievable(self):
|
|
"""Return True if properties can be retrieved."""
|
|
return True
|
|
|
|
def properties_proactively_reported(self):
|
|
"""Return True if properties asynchronously reported."""
|
|
return True
|
|
|
|
def get_property(self, name):
|
|
"""Read and return a property."""
|
|
if name != 'lockState':
|
|
raise UnsupportedProperty(name)
|
|
|
|
if self.entity.state == STATE_LOCKED:
|
|
return 'LOCKED'
|
|
if self.entity.state == STATE_UNLOCKED:
|
|
return 'UNLOCKED'
|
|
return 'JAMMED'
|
|
|
|
|
|
class AlexaSceneController(AlexaCapibility):
|
|
"""Implements Alexa.SceneController.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-scenecontroller.html
|
|
"""
|
|
|
|
def __init__(self, entity, supports_deactivation):
|
|
"""Initialize the entity."""
|
|
super().__init__(entity)
|
|
self.supports_deactivation = lambda: supports_deactivation
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.SceneController'
|
|
|
|
|
|
class AlexaBrightnessController(AlexaCapibility):
|
|
"""Implements Alexa.BrightnessController.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-brightnesscontroller.html
|
|
"""
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.BrightnessController'
|
|
|
|
def properties_supported(self):
|
|
"""Return what properties this entity supports."""
|
|
return [{'name': 'brightness'}]
|
|
|
|
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 != 'brightness':
|
|
raise UnsupportedProperty(name)
|
|
if 'brightness' in self.entity.attributes:
|
|
return round(self.entity.attributes['brightness'] / 255.0 * 100)
|
|
return 0
|
|
|
|
|
|
class AlexaColorController(AlexaCapibility):
|
|
"""Implements Alexa.ColorController.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-colorcontroller.html
|
|
"""
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.ColorController'
|
|
|
|
def properties_supported(self):
|
|
"""Return what properties this entity supports."""
|
|
return [{'name': 'color'}]
|
|
|
|
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 != 'color':
|
|
raise UnsupportedProperty(name)
|
|
|
|
hue, saturation = self.entity.attributes.get(
|
|
light.ATTR_HS_COLOR, (0, 0))
|
|
|
|
return {
|
|
'hue': hue,
|
|
'saturation': saturation / 100.0,
|
|
'brightness': self.entity.attributes.get(
|
|
light.ATTR_BRIGHTNESS, 0) / 255.0,
|
|
}
|
|
|
|
|
|
class AlexaColorTemperatureController(AlexaCapibility):
|
|
"""Implements Alexa.ColorTemperatureController.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-colortemperaturecontroller.html
|
|
"""
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.ColorTemperatureController'
|
|
|
|
def properties_supported(self):
|
|
"""Return what properties this entity supports."""
|
|
return [{'name': 'colorTemperatureInKelvin'}]
|
|
|
|
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 != 'colorTemperatureInKelvin':
|
|
raise UnsupportedProperty(name)
|
|
if 'color_temp' in self.entity.attributes:
|
|
return color_util.color_temperature_mired_to_kelvin(
|
|
self.entity.attributes['color_temp'])
|
|
return 0
|
|
|
|
|
|
class AlexaPercentageController(AlexaCapibility):
|
|
"""Implements Alexa.PercentageController.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-percentagecontroller.html
|
|
"""
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.PercentageController'
|
|
|
|
def properties_supported(self):
|
|
"""Return what properties this entity supports."""
|
|
return [{'name': 'percentage'}]
|
|
|
|
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 != 'percentage':
|
|
raise UnsupportedProperty(name)
|
|
|
|
if self.entity.domain == fan.DOMAIN:
|
|
speed = self.entity.attributes.get(fan.ATTR_SPEED)
|
|
|
|
return PERCENTAGE_FAN_MAP.get(speed, 0)
|
|
|
|
if self.entity.domain == cover.DOMAIN:
|
|
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION, 0)
|
|
|
|
return 0
|
|
|
|
|
|
class AlexaSpeaker(AlexaCapibility):
|
|
"""Implements Alexa.Speaker.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-speaker.html
|
|
"""
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.Speaker'
|
|
|
|
|
|
class AlexaStepSpeaker(AlexaCapibility):
|
|
"""Implements Alexa.StepSpeaker.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html
|
|
"""
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.StepSpeaker'
|
|
|
|
|
|
class AlexaPlaybackController(AlexaCapibility):
|
|
"""Implements Alexa.PlaybackController.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html
|
|
"""
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.PlaybackController'
|
|
|
|
|
|
class AlexaInputController(AlexaCapibility):
|
|
"""Implements Alexa.InputController.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html
|
|
"""
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.InputController'
|
|
|
|
|
|
class AlexaTemperatureSensor(AlexaCapibility):
|
|
"""Implements Alexa.TemperatureSensor.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-temperaturesensor.html
|
|
"""
|
|
|
|
def __init__(self, hass, entity):
|
|
"""Initialize the entity."""
|
|
super().__init__(entity)
|
|
self.hass = hass
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.TemperatureSensor'
|
|
|
|
def properties_supported(self):
|
|
"""Return what properties this entity supports."""
|
|
return [{'name': 'temperature'}]
|
|
|
|
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 != 'temperature':
|
|
raise UnsupportedProperty(name)
|
|
|
|
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
|
temp = 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)
|
|
return {
|
|
'value': float(temp),
|
|
'scale': API_TEMP_UNITS[unit],
|
|
}
|
|
|
|
|
|
class AlexaContactSensor(AlexaCapibility):
|
|
"""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
|
|
"""
|
|
|
|
def __init__(self, hass, entity):
|
|
"""Initialize the entity."""
|
|
super().__init__(entity)
|
|
self.hass = hass
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.ContactSensor'
|
|
|
|
def properties_supported(self):
|
|
"""Return what properties this entity supports."""
|
|
return [{'name': 'detectionState'}]
|
|
|
|
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 != 'detectionState':
|
|
raise UnsupportedProperty(name)
|
|
|
|
if self.entity.state == STATE_ON:
|
|
return 'DETECTED'
|
|
return 'NOT_DETECTED'
|
|
|
|
|
|
class AlexaMotionSensor(AlexaCapibility):
|
|
"""Implements Alexa.MotionSensor.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html
|
|
"""
|
|
|
|
def __init__(self, hass, entity):
|
|
"""Initialize the entity."""
|
|
super().__init__(entity)
|
|
self.hass = hass
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.MotionSensor'
|
|
|
|
def properties_supported(self):
|
|
"""Return what properties this entity supports."""
|
|
return [{'name': 'detectionState'}]
|
|
|
|
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 != 'detectionState':
|
|
raise UnsupportedProperty(name)
|
|
|
|
if self.entity.state == STATE_ON:
|
|
return 'DETECTED'
|
|
return 'NOT_DETECTED'
|
|
|
|
|
|
class AlexaThermostatController(AlexaCapibility):
|
|
"""Implements Alexa.ThermostatController.
|
|
|
|
https://developer.amazon.com/docs/device-apis/alexa-thermostatcontroller.html
|
|
"""
|
|
|
|
def __init__(self, hass, entity):
|
|
"""Initialize the entity."""
|
|
super().__init__(entity)
|
|
self.hass = hass
|
|
|
|
def name(self):
|
|
"""Return the Alexa API name of this interface."""
|
|
return 'Alexa.ThermostatController'
|
|
|
|
def properties_supported(self):
|
|
"""Return what properties this entity supports."""
|
|
properties = []
|
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
|
|
properties.append({'name': 'targetSetpoint'})
|
|
if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW:
|
|
properties.append({'name': 'lowerSetpoint'})
|
|
if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
|
|
properties.append({'name': 'upperSetpoint'})
|
|
if supported & climate.SUPPORT_OPERATION_MODE:
|
|
properties.append({'name': 'thermostatMode'})
|
|
return properties
|
|
|
|
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 == 'thermostatMode':
|
|
ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
|
|
mode = API_THERMOSTAT_MODES.get(ha_mode)
|
|
if mode is None:
|
|
_LOGGER.error("%s (%s) has unsupported %s value '%s'",
|
|
self.entity.entity_id, type(self.entity),
|
|
climate.ATTR_OPERATION_MODE, ha_mode)
|
|
raise UnsupportedProperty(name)
|
|
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
|
|
|
|
return {
|
|
'value': float(temp),
|
|
'scale': API_TEMP_UNITS[unit],
|
|
}
|