"""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, API_THERMOSTAT_PRESETS, 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.domain == climate.DOMAIN: is_on = self.entity.state != climate.HVAC_MODE_OFF else: is_on = self.entity.state != STATE_OFF return 'ON' if is_on else 'OFF' 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 = [{'name': 'thermostatMode'}] 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_RANGE: properties.append({'name': 'lowerSetpoint'}) properties.append({'name': 'upperSetpoint'}) 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': preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE) if preset in API_THERMOSTAT_PRESETS: mode = API_THERMOSTAT_PRESETS[preset] else: mode = API_THERMOSTAT_MODES.get(self.entity.state) if mode is None: _LOGGER.error( "%s (%s) has unsupported state value '%s'", self.entity.entity_id, type(self.entity), self.entity.state) 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], }