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"
This commit is contained in:
ochlocracy 2019-12-14 02:47:45 -05:00 committed by Paulus Schoutsen
parent 3c86825e25
commit 3db7e8f5e9
5 changed files with 217 additions and 3 deletions

View file

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

View file

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

View file

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

View file

@ -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"},
)

View file

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