From 3db7e8f5e9218a5721e8cc46e32e8e9969b6ce2e Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Sat, 14 Dec 2019 02:47:45 -0500 Subject: [PATCH] Implement Alexa.EventDetectionSensor for Alexa (#28276) * Implement Alexa.EventDetectionSensor Interface * Removed references to PR #28218 not yet merged into dev. * Update tests to include Alexa Interface * Guard for `unknown` and `unavailible` states. * Fixed Unnecessary "elif" after "return" --- .../components/alexa/capabilities.py | 59 +++++++++++++- homeassistant/components/alexa/entities.py | 37 ++++++++- tests/components/alexa/__init__.py | 1 + tests/components/alexa/test_capabilities.py | 42 ++++++++++ tests/components/alexa/test_smart_home.py | 81 +++++++++++++++++++ 5 files changed, 217 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 9f217d2e9c9..b5ffb1ef7e6 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,7 +1,7 @@ """Alexa capabilities.""" import logging -from homeassistant.components import cover, fan, light +from homeassistant.components import cover, fan, image_processing, light from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER import homeassistant.components.climate.const as climate import homeassistant.components.media_player.const as media_player @@ -1265,3 +1265,60 @@ class AlexaSeekController(AlexaCapability): def name(self): """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 + """ + + 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.EventDetectionSensor" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "humanPresenceDetectionState"}] + + 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 != "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: + human_presence = "DETECTED" + + return {"value": human_presence} + + def configuration(self): + """Return supported detection types.""" + return { + "detectionMethods": ["AUDIO", "VIDEO"], + "detectionModes": { + "humanPresence": { + "featureAvailability": "ENABLED", + "supportsNotDetected": True, + } + }, + } diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 733ea73f998..017686df607 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -9,6 +9,7 @@ from homeassistant.components import ( cover, fan, group, + image_processing, input_boolean, light, lock, @@ -40,6 +41,7 @@ from .capabilities import ( AlexaContactSensor, AlexaDoorbellEventSource, AlexaEndpointHealth, + AlexaEventDetectionSensor, AlexaInputController, AlexaLockController, AlexaModeController, @@ -522,6 +524,7 @@ class BinarySensorCapabilities(AlexaEntity): TYPE_CONTACT = "contact" TYPE_MOTION = "motion" + TYPE_PRESENCE = "presence" def default_display_categories(self): """Return the display categories for this entity.""" @@ -530,6 +533,8 @@ class BinarySensorCapabilities(AlexaEntity): return [DisplayCategory.CONTACT_SENSOR] if sensor_type is self.TYPE_MOTION: return [DisplayCategory.MOTION_SENSOR] + if sensor_type is self.TYPE_PRESENCE: + return [DisplayCategory.CAMERA] def interfaces(self): """Yield the supported interfaces.""" @@ -538,7 +543,10 @@ class BinarySensorCapabilities(AlexaEntity): yield AlexaContactSensor(self.hass, self.entity) elif sensor_type is self.TYPE_MOTION: yield AlexaMotionSensor(self.hass, self.entity) + elif sensor_type is self.TYPE_PRESENCE: + yield AlexaEventDetectionSensor(self.hass, self.entity) + # yield additional interfaces based on specified display category in config. entity_conf = self.config.entity_config.get(self.entity.entity_id, {}) if CONF_DISPLAY_CATEGORIES in entity_conf: if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL: @@ -547,6 +555,8 @@ class BinarySensorCapabilities(AlexaEntity): yield AlexaContactSensor(self.hass, self.entity) elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.MOTION_SENSOR: yield AlexaMotionSensor(self.hass, self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CAMERA: + yield AlexaEventDetectionSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) @@ -554,11 +564,20 @@ class BinarySensorCapabilities(AlexaEntity): def get_type(self): """Return the type of binary sensor.""" attrs = self.entity.attributes - if attrs.get(ATTR_DEVICE_CLASS) in ("door", "garage_door", "opening", "window"): + if attrs.get(ATTR_DEVICE_CLASS) in ( + binary_sensor.DEVICE_CLASS_DOOR, + binary_sensor.DEVICE_CLASS_GARAGE_DOOR, + binary_sensor.DEVICE_CLASS_OPENING, + binary_sensor.DEVICE_CLASS_WINDOW, + ): return self.TYPE_CONTACT - if attrs.get(ATTR_DEVICE_CLASS) == "motion": + + if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_MOTION: return self.TYPE_MOTION + if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_PRESENCE: + return self.TYPE_PRESENCE + @ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN) class AlarmControlPanelCapabilities(AlexaEntity): @@ -574,3 +593,17 @@ class AlarmControlPanelCapabilities(AlexaEntity): yield AlexaSecurityPanelController(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(image_processing.DOMAIN) +class ImageProcessingCapabilities(AlexaEntity): + """Class to represent image_processing capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.CAMERA] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaEventDetectionSensor(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 79fdb86c3ef..473a3c6e12b 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -17,6 +17,7 @@ class MockConfig(config.AbstractConfig): "binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}, "binary_sensor.test_contact_forced": {"display_categories": "CONTACT_SENSOR"}, "binary_sensor.test_motion_forced": {"display_categories": "MOTION_SENSOR"}, + "binary_sensor.test_motion_camera_event": {"display_categories": "CAMERA"}, } @property diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 952af543aab..ab9c375103a 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -667,3 +667,45 @@ async def test_report_playback_state(hass): properties.assert_equal( "Alexa.PlaybackStateReporter", "playbackState", {"state": "STOPPED"} ) + + +async def test_report_image_processing(hass): + """Test EventDetectionSensor implements humanPresenceDetectionState property.""" + hass.states.async_set( + "image_processing.test_face", + 0, + { + "friendly_name": "Test face", + "device_class": "face", + "faces": [], + "total_faces": 0, + }, + ) + + properties = await reported_properties(hass, "image_processing#test_face") + properties.assert_equal( + "Alexa.EventDetectionSensor", + "humanPresenceDetectionState", + {"value": "NOT_DETECTED"}, + ) + + hass.states.async_set( + "image_processing.test_classifier", + 3, + { + "friendly_name": "Test classifier", + "device_class": "face", + "faces": [ + {"confidence": 98.34, "name": "Hans", "age": 16.0, "gender": "male"}, + {"name": "Helena", "age": 28.0, "gender": "female"}, + {"confidence": 62.53, "name": "Luna"}, + ], + "total_faces": 3, + }, + ) + properties = await reported_properties(hass, "image_processing#test_classifier") + properties.assert_equal( + "Alexa.EventDetectionSensor", + "humanPresenceDetectionState", + {"value": "DETECTED"}, + ) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index ea1e6f50fcf..25c8f2a864f 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2386,3 +2386,84 @@ async def test_cover_position(hass): assert properties["name"] == "mode" assert properties["namespace"] == "Alexa.ModeController" assert properties["value"] == "position.open" + + +async def test_image_processing(hass): + """Test image_processing discovery as event detection.""" + device = ( + "image_processing.test_face", + 0, + { + "friendly_name": "Test face", + "device_class": "face", + "faces": [], + "total_faces": 0, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "image_processing#test_face" + assert appliance["displayCategories"][0] == "CAMERA" + assert appliance["friendlyName"] == "Test face" + + assert_endpoint_capabilities( + appliance, "Alexa.EventDetectionSensor", "Alexa.EndpointHealth" + ) + + +async def test_motion_sensor_event_detection(hass): + """Test motion sensor with EventDetectionSensor discovery.""" + device = ( + "binary_sensor.test_motion_camera_event", + "off", + {"friendly_name": "Test motion camera event", "device_class": "motion"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_motion_camera_event" + assert appliance["displayCategories"][0] == "CAMERA" + assert appliance["friendlyName"] == "Test motion camera event" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa", + "Alexa.MotionSensor", + "Alexa.EventDetectionSensor", + "Alexa.EndpointHealth", + ) + + event_detection_capability = get_capability( + capabilities, "Alexa.EventDetectionSensor" + ) + assert event_detection_capability is not None + properties = event_detection_capability["properties"] + assert properties["proactivelyReported"] is True + assert not properties["retrievable"] + assert {"name": "humanPresenceDetectionState"} in properties["supported"] + + +async def test_presence_sensor(hass): + """Test presence sensor.""" + device = ( + "binary_sensor.test_presence_sensor", + "off", + {"friendly_name": "Test presence sensor", "device_class": "presence"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_presence_sensor" + assert appliance["displayCategories"][0] == "CAMERA" + assert appliance["friendlyName"] == "Test presence sensor" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa", "Alexa.EventDetectionSensor", "Alexa.EndpointHealth" + ) + + event_detection_capability = get_capability( + capabilities, "Alexa.EventDetectionSensor" + ) + assert event_detection_capability is not None + properties = event_detection_capability["properties"] + assert properties["proactivelyReported"] is True + assert not properties["retrievable"] + assert {"name": "humanPresenceDetectionState"} in properties["supported"]