Rebase Implement Alexa.DoorbellEventSource Interface Controller (#27726)

pull/28148/head
ochlocracy 2019-10-23 14:41:26 -04:00 committed by Paulus Schoutsen
parent 0771dc3a37
commit 65d8c70377
6 changed files with 161 additions and 10 deletions

View File

@ -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,17 +112,21 @@ 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()
if non_controllable is not None: if non_controllable is not None:
@ -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

View File

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

View File

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

View File

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

View File

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

View File

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