Rebase Implement Alexa.DoorbellEventSource Interface Controller (#27726)
parent
0771dc3a37
commit
65d8c70377
|
@ -91,6 +91,15 @@ class AlexaCapability:
|
||||||
"""Applicable only to scenes."""
|
"""Applicable only to scenes."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def capability_proactively_reported():
|
||||||
|
"""Return True if the capability is proactively reported.
|
||||||
|
|
||||||
|
Set properties_proactively_reported() for proactively reported properties.
|
||||||
|
Applicable to DoorbellEventSource.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def capability_resources():
|
def capability_resources():
|
||||||
"""Applicable to ToggleController, RangeController, and ModeController interfaces."""
|
"""Applicable to ToggleController, RangeController, and ModeController interfaces."""
|
||||||
|
@ -103,16 +112,20 @@ class AlexaCapability:
|
||||||
|
|
||||||
def serialize_discovery(self):
|
def serialize_discovery(self):
|
||||||
"""Serialize according to the Discovery API."""
|
"""Serialize according to the Discovery API."""
|
||||||
result = {
|
result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"}
|
||||||
"type": "AlexaInterface",
|
|
||||||
"interface": self.name(),
|
properties_supported = self.properties_supported()
|
||||||
"version": "3",
|
if properties_supported:
|
||||||
"properties": {
|
result["properties"] = {
|
||||||
"supported": self.properties_supported(),
|
"supported": self.properties_supported(),
|
||||||
"proactivelyReported": self.properties_proactively_reported(),
|
"proactivelyReported": self.properties_proactively_reported(),
|
||||||
"retrievable": self.properties_retrievable(),
|
"retrievable": self.properties_retrievable(),
|
||||||
},
|
}
|
||||||
}
|
|
||||||
|
# pylint: disable=assignment-from-none
|
||||||
|
proactively_reported = self.capability_proactively_reported()
|
||||||
|
if proactively_reported is not None:
|
||||||
|
result["proactivelyReported"] = proactively_reported
|
||||||
|
|
||||||
# pylint: disable=assignment-from-none
|
# pylint: disable=assignment-from-none
|
||||||
non_controllable = self.properties_non_controllable()
|
non_controllable = self.properties_non_controllable()
|
||||||
|
@ -1050,3 +1063,18 @@ class AlexaChannelController(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.ChannelController"
|
return "Alexa.ChannelController"
|
||||||
|
|
||||||
|
|
||||||
|
class AlexaDoorbellEventSource(AlexaCapability):
|
||||||
|
"""Implements Alexa.DoorbellEventSource.
|
||||||
|
|
||||||
|
https://developer.amazon.com/docs/device-apis/alexa-doorbelleventsource.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
"""Return the Alexa API name of this interface."""
|
||||||
|
return "Alexa.DoorbellEventSource"
|
||||||
|
|
||||||
|
def capability_proactively_reported(self):
|
||||||
|
"""Return True for proactively reported capability."""
|
||||||
|
return True
|
||||||
|
|
|
@ -38,6 +38,7 @@ from .capabilities import (
|
||||||
AlexaColorController,
|
AlexaColorController,
|
||||||
AlexaColorTemperatureController,
|
AlexaColorTemperatureController,
|
||||||
AlexaContactSensor,
|
AlexaContactSensor,
|
||||||
|
AlexaDoorbellEventSource,
|
||||||
AlexaEndpointHealth,
|
AlexaEndpointHealth,
|
||||||
AlexaInputController,
|
AlexaInputController,
|
||||||
AlexaLockController,
|
AlexaLockController,
|
||||||
|
@ -84,7 +85,7 @@ class DisplayCategory:
|
||||||
DOOR = "DOOR"
|
DOOR = "DOOR"
|
||||||
|
|
||||||
# Indicates a doorbell.
|
# Indicates a doorbell.
|
||||||
DOOR_BELL = "DOORBELL"
|
DOORBELL = "DOORBELL"
|
||||||
|
|
||||||
# Indicates a fan.
|
# Indicates a fan.
|
||||||
FAN = "FAN"
|
FAN = "FAN"
|
||||||
|
@ -500,6 +501,11 @@ class BinarySensorCapabilities(AlexaEntity):
|
||||||
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)
|
||||||
|
|
||||||
|
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:
|
||||||
|
yield AlexaDoorbellEventSource(self.entity)
|
||||||
|
|
||||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
|
|
||||||
def get_type(self):
|
def get_type(self):
|
||||||
|
|
|
@ -6,7 +6,8 @@ import logging
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
|
||||||
from homeassistant.const import MATCH_ALL
|
import homeassistant.util.dt as dt_util
|
||||||
|
from homeassistant.const import MATCH_ALL, STATE_ON
|
||||||
|
|
||||||
from .const import API_CHANGE, Cause
|
from .const import API_CHANGE, Cause
|
||||||
from .entities import ENTITY_ADAPTERS
|
from .entities import ENTITY_ADAPTERS
|
||||||
|
@ -45,6 +46,14 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
||||||
hass, smart_home_config, alexa_changed_entity
|
hass, smart_home_config, alexa_changed_entity
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
if (
|
||||||
|
interface.name() == "Alexa.DoorbellEventSource"
|
||||||
|
and new_state.state == STATE_ON
|
||||||
|
):
|
||||||
|
await async_send_doorbell_event_message(
|
||||||
|
hass, smart_home_config, alexa_changed_entity
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
return hass.helpers.event.async_track_state_change(
|
return hass.helpers.event.async_track_state_change(
|
||||||
MATCH_ALL, async_entity_state_listener
|
MATCH_ALL, async_entity_state_listener
|
||||||
|
@ -184,3 +193,58 @@ async def async_send_delete_message(hass, config, entity_ids):
|
||||||
return await session.post(
|
return await session.post(
|
||||||
config.endpoint, headers=headers, json=message_serialized, allow_redirects=True
|
config.endpoint, headers=headers, json=message_serialized, allow_redirects=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_send_doorbell_event_message(hass, config, alexa_entity):
|
||||||
|
"""Send a DoorbellPress event message for an Alexa entity.
|
||||||
|
|
||||||
|
https://developer.amazon.com/docs/smarthome/send-events-to-the-alexa-event-gateway.html
|
||||||
|
"""
|
||||||
|
token = await config.async_get_access_token()
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
endpoint = alexa_entity.alexa_id()
|
||||||
|
|
||||||
|
message = AlexaResponse(
|
||||||
|
name="DoorbellPress",
|
||||||
|
namespace="Alexa.DoorbellEventSource",
|
||||||
|
payload={
|
||||||
|
"cause": {"type": Cause.PHYSICAL_INTERACTION},
|
||||||
|
"timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
message.set_endpoint_full(token, endpoint)
|
||||||
|
|
||||||
|
message_serialized = message.serialize()
|
||||||
|
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||||
|
response = await session.post(
|
||||||
|
config.endpoint,
|
||||||
|
headers=headers,
|
||||||
|
json=message_serialized,
|
||||||
|
allow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
|
_LOGGER.error("Timeout sending report to Alexa.")
|
||||||
|
return
|
||||||
|
|
||||||
|
response_text = await response.text()
|
||||||
|
|
||||||
|
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
|
||||||
|
_LOGGER.debug("Received (%s): %s", response.status, response_text)
|
||||||
|
|
||||||
|
if response.status == 202:
|
||||||
|
return
|
||||||
|
|
||||||
|
response_json = json.loads(response_text)
|
||||||
|
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error when sending DoorbellPress event to Alexa: %s: %s",
|
||||||
|
response_json["payload"]["code"],
|
||||||
|
response_json["payload"]["description"],
|
||||||
|
)
|
||||||
|
|
|
@ -13,7 +13,7 @@ TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token"
|
||||||
class MockConfig(config.AbstractConfig):
|
class MockConfig(config.AbstractConfig):
|
||||||
"""Mock Alexa config."""
|
"""Mock Alexa config."""
|
||||||
|
|
||||||
entity_config = {}
|
entity_config = {"binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supports_auth(self):
|
def supports_auth(self):
|
||||||
|
|
|
@ -1196,6 +1196,28 @@ async def test_motion_sensor(hass):
|
||||||
properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED")
|
properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_doorbell_sensor(hass):
|
||||||
|
"""Test doorbell sensor discovery."""
|
||||||
|
device = (
|
||||||
|
"binary_sensor.test_doorbell",
|
||||||
|
"off",
|
||||||
|
{"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"},
|
||||||
|
)
|
||||||
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
|
assert appliance["endpointId"] == "binary_sensor#test_doorbell"
|
||||||
|
assert appliance["displayCategories"][0] == "DOORBELL"
|
||||||
|
assert appliance["friendlyName"] == "Test Doorbell Sensor"
|
||||||
|
|
||||||
|
capabilities = assert_endpoint_capabilities(
|
||||||
|
appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth"
|
||||||
|
)
|
||||||
|
|
||||||
|
doorbell_capability = get_capability(capabilities, "Alexa.DoorbellEventSource")
|
||||||
|
assert doorbell_capability is not None
|
||||||
|
assert doorbell_capability["proactivelyReported"] is True
|
||||||
|
|
||||||
|
|
||||||
async def test_unknown_sensor(hass):
|
async def test_unknown_sensor(hass):
|
||||||
"""Test sensors of unknown quantities are not discovered."""
|
"""Test sensors of unknown quantities are not discovered."""
|
||||||
device = (
|
device = (
|
||||||
|
|
|
@ -137,3 +137,34 @@ async def test_send_delete_message(hass, aioclient_mock):
|
||||||
call_json["event"]["payload"]["endpoints"][0]["endpointId"]
|
call_json["event"]["payload"]["endpoints"][0]["endpointId"]
|
||||||
== "binary_sensor#test_contact"
|
== "binary_sensor#test_contact"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_doorbell_event(hass, aioclient_mock):
|
||||||
|
"""Test doorbell press reports."""
|
||||||
|
aioclient_mock.post(TEST_URL, text="", status=202)
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
"binary_sensor.test_doorbell",
|
||||||
|
"off",
|
||||||
|
{"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"},
|
||||||
|
)
|
||||||
|
|
||||||
|
await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
"binary_sensor.test_doorbell",
|
||||||
|
"on",
|
||||||
|
{"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# To trigger event listener
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
call = aioclient_mock.mock_calls
|
||||||
|
|
||||||
|
call_json = call[0][2]
|
||||||
|
assert call_json["event"]["header"]["namespace"] == "Alexa.DoorbellEventSource"
|
||||||
|
assert call_json["event"]["header"]["name"] == "DoorbellPress"
|
||||||
|
assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION"
|
||||||
|
assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell"
|
||||||
|
|
Loading…
Reference in New Issue