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:
parent
3c86825e25
commit
3db7e8f5e9
5 changed files with 217 additions and 3 deletions
|
@ -1,7 +1,7 @@
|
||||||
"""Alexa capabilities."""
|
"""Alexa capabilities."""
|
||||||
import logging
|
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
|
from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER
|
||||||
import homeassistant.components.climate.const as climate
|
import homeassistant.components.climate.const as climate
|
||||||
import homeassistant.components.media_player.const as media_player
|
import homeassistant.components.media_player.const as media_player
|
||||||
|
@ -1265,3 +1265,60 @@ class AlexaSeekController(AlexaCapability):
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the Alexa API name of this interface."""
|
"""Return the Alexa API name of this interface."""
|
||||||
return "Alexa.SeekController"
|
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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ from homeassistant.components import (
|
||||||
cover,
|
cover,
|
||||||
fan,
|
fan,
|
||||||
group,
|
group,
|
||||||
|
image_processing,
|
||||||
input_boolean,
|
input_boolean,
|
||||||
light,
|
light,
|
||||||
lock,
|
lock,
|
||||||
|
@ -40,6 +41,7 @@ from .capabilities import (
|
||||||
AlexaContactSensor,
|
AlexaContactSensor,
|
||||||
AlexaDoorbellEventSource,
|
AlexaDoorbellEventSource,
|
||||||
AlexaEndpointHealth,
|
AlexaEndpointHealth,
|
||||||
|
AlexaEventDetectionSensor,
|
||||||
AlexaInputController,
|
AlexaInputController,
|
||||||
AlexaLockController,
|
AlexaLockController,
|
||||||
AlexaModeController,
|
AlexaModeController,
|
||||||
|
@ -522,6 +524,7 @@ class BinarySensorCapabilities(AlexaEntity):
|
||||||
|
|
||||||
TYPE_CONTACT = "contact"
|
TYPE_CONTACT = "contact"
|
||||||
TYPE_MOTION = "motion"
|
TYPE_MOTION = "motion"
|
||||||
|
TYPE_PRESENCE = "presence"
|
||||||
|
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
"""Return the display categories for this entity."""
|
"""Return the display categories for this entity."""
|
||||||
|
@ -530,6 +533,8 @@ class BinarySensorCapabilities(AlexaEntity):
|
||||||
return [DisplayCategory.CONTACT_SENSOR]
|
return [DisplayCategory.CONTACT_SENSOR]
|
||||||
if sensor_type is self.TYPE_MOTION:
|
if sensor_type is self.TYPE_MOTION:
|
||||||
return [DisplayCategory.MOTION_SENSOR]
|
return [DisplayCategory.MOTION_SENSOR]
|
||||||
|
if sensor_type is self.TYPE_PRESENCE:
|
||||||
|
return [DisplayCategory.CAMERA]
|
||||||
|
|
||||||
def interfaces(self):
|
def interfaces(self):
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
|
@ -538,7 +543,10 @@ class BinarySensorCapabilities(AlexaEntity):
|
||||||
yield AlexaContactSensor(self.hass, self.entity)
|
yield AlexaContactSensor(self.hass, self.entity)
|
||||||
elif sensor_type is self.TYPE_MOTION:
|
elif sensor_type is self.TYPE_MOTION:
|
||||||
yield AlexaMotionSensor(self.hass, self.entity)
|
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, {})
|
entity_conf = self.config.entity_config.get(self.entity.entity_id, {})
|
||||||
if CONF_DISPLAY_CATEGORIES in entity_conf:
|
if CONF_DISPLAY_CATEGORIES in entity_conf:
|
||||||
if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL:
|
if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL:
|
||||||
|
@ -547,6 +555,8 @@ class BinarySensorCapabilities(AlexaEntity):
|
||||||
yield AlexaContactSensor(self.hass, self.entity)
|
yield AlexaContactSensor(self.hass, self.entity)
|
||||||
elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.MOTION_SENSOR:
|
elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.MOTION_SENSOR:
|
||||||
yield AlexaMotionSensor(self.hass, self.entity)
|
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 AlexaEndpointHealth(self.hass, self.entity)
|
||||||
yield Alexa(self.hass)
|
yield Alexa(self.hass)
|
||||||
|
@ -554,11 +564,20 @@ class BinarySensorCapabilities(AlexaEntity):
|
||||||
def get_type(self):
|
def get_type(self):
|
||||||
"""Return the type of binary sensor."""
|
"""Return the type of binary sensor."""
|
||||||
attrs = self.entity.attributes
|
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
|
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
|
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)
|
@ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN)
|
||||||
class AlarmControlPanelCapabilities(AlexaEntity):
|
class AlarmControlPanelCapabilities(AlexaEntity):
|
||||||
|
@ -574,3 +593,17 @@ class AlarmControlPanelCapabilities(AlexaEntity):
|
||||||
yield AlexaSecurityPanelController(self.hass, self.entity)
|
yield AlexaSecurityPanelController(self.hass, self.entity)
|
||||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
yield Alexa(self.hass)
|
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)
|
||||||
|
|
|
@ -17,6 +17,7 @@ class MockConfig(config.AbstractConfig):
|
||||||
"binary_sensor.test_doorbell": {"display_categories": "DOORBELL"},
|
"binary_sensor.test_doorbell": {"display_categories": "DOORBELL"},
|
||||||
"binary_sensor.test_contact_forced": {"display_categories": "CONTACT_SENSOR"},
|
"binary_sensor.test_contact_forced": {"display_categories": "CONTACT_SENSOR"},
|
||||||
"binary_sensor.test_motion_forced": {"display_categories": "MOTION_SENSOR"},
|
"binary_sensor.test_motion_forced": {"display_categories": "MOTION_SENSOR"},
|
||||||
|
"binary_sensor.test_motion_camera_event": {"display_categories": "CAMERA"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -667,3 +667,45 @@ async def test_report_playback_state(hass):
|
||||||
properties.assert_equal(
|
properties.assert_equal(
|
||||||
"Alexa.PlaybackStateReporter", "playbackState", {"state": "STOPPED"}
|
"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"},
|
||||||
|
)
|
||||||
|
|
|
@ -2386,3 +2386,84 @@ async def test_cover_position(hass):
|
||||||
assert properties["name"] == "mode"
|
assert properties["name"] == "mode"
|
||||||
assert properties["namespace"] == "Alexa.ModeController"
|
assert properties["namespace"] == "Alexa.ModeController"
|
||||||
assert properties["value"] == "position.open"
|
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"]
|
||||||
|
|
Loading…
Add table
Reference in a new issue