mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Add SecurityPanelController for alarm_control_panel to alexa (#27081)
* Implemented Alexa.SecurityPanelController Interface for alarm_control_panel https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html * Implemented Tests for Alexa.SecurityPanelController Interface for alarm_control_panel * Added additional AuthorizationRequired error handling * Removed optional exitDelayInSeconds * Updating elif to if to please pylint * Adding self to code owners. * Adding self to code owners. * Added AlexaEndpointHealth Interface to alarm_control_panel entities. * Added additional entity tests. * Code reformatted with Black. * Updated alexa alarm_control_panel tests for more coverage. * Updated alexa alarm_control_panel tests for more coverage. Fixed Test. * Adding self to code owners.
This commit is contained in:
parent
f169e84d21
commit
9a5c1fbaed
@ -17,7 +17,7 @@ homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/airly/* @bieniu
|
||||
homeassistant/components/airvisual/* @bachya
|
||||
homeassistant/components/alarm_control_panel/* @colinodell
|
||||
homeassistant/components/alexa/* @home-assistant/cloud
|
||||
homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy
|
||||
homeassistant/components/alpha_vantage/* @fabaff
|
||||
homeassistant/components/amazon_polly/* @robbiet480
|
||||
homeassistant/components/ambiclimate/* @danielhiversen
|
||||
|
@ -5,6 +5,10 @@ from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_LOCKED,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
@ -13,6 +17,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
import homeassistant.components.climate.const as climate
|
||||
from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER
|
||||
from homeassistant.components import light, fan, cover
|
||||
import homeassistant.util.color as color_util
|
||||
import homeassistant.util.dt as dt_util
|
||||
@ -79,6 +84,11 @@ class AlexaCapibility:
|
||||
"""Applicable only to scenes."""
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def configuration():
|
||||
"""Applicable only to security control panel."""
|
||||
return []
|
||||
|
||||
def serialize_discovery(self):
|
||||
"""Serialize according to the Discovery API."""
|
||||
result = {
|
||||
@ -96,6 +106,11 @@ class AlexaCapibility:
|
||||
supports_deactivation = self.supports_deactivation()
|
||||
if supports_deactivation is not None:
|
||||
result["supportsDeactivation"] = supports_deactivation
|
||||
|
||||
configuration = self.configuration()
|
||||
if configuration:
|
||||
result["configuration"] = configuration
|
||||
|
||||
return result
|
||||
|
||||
def serialize_properties(self):
|
||||
@ -649,3 +664,55 @@ class AlexaPowerLevelController(AlexaCapibility):
|
||||
return PERCENTAGE_FAN_MAP.get(speed, None)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class AlexaSecurityPanelController(AlexaCapibility):
|
||||
"""Implements Alexa.SecurityPanelController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.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.SecurityPanelController"
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{"name": "armState"}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != "armState":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
arm_state = self.entity.state
|
||||
if arm_state == STATE_ALARM_ARMED_HOME:
|
||||
return "ARMED_STAY"
|
||||
if arm_state == STATE_ALARM_ARMED_AWAY:
|
||||
return "ARMED_AWAY"
|
||||
if arm_state == STATE_ALARM_ARMED_NIGHT:
|
||||
return "ARMED_NIGHT"
|
||||
if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
|
||||
return "ARMED_STAY"
|
||||
return "DISARMED"
|
||||
|
||||
def configuration(self):
|
||||
"""Return supported authorization types."""
|
||||
code_format = self.entity.attributes.get(ATTR_CODE_FORMAT)
|
||||
|
||||
if code_format == FORMAT_NUMBER:
|
||||
return {"supportedAuthorizationTypes": [{"type": "FOUR_DIGIT_PIN"}]}
|
||||
return []
|
||||
|
@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.components import (
|
||||
alarm_control_panel,
|
||||
alert,
|
||||
automation,
|
||||
binary_sensor,
|
||||
@ -45,6 +46,7 @@ from .capabilities import (
|
||||
AlexaPowerController,
|
||||
AlexaPowerLevelController,
|
||||
AlexaSceneController,
|
||||
AlexaSecurityPanelController,
|
||||
AlexaSpeaker,
|
||||
AlexaStepSpeaker,
|
||||
AlexaTemperatureSensor,
|
||||
@ -487,3 +489,18 @@ class BinarySensorCapabilities(AlexaEntity):
|
||||
return self.TYPE_CONTACT
|
||||
if attrs.get(ATTR_DEVICE_CLASS) == "motion":
|
||||
return self.TYPE_MOTION
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN)
|
||||
class AlarmControlPanelCapabilities(AlexaEntity):
|
||||
"""Class to represent Alarm capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.SECURITY_PANEL]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
if not self.entity.attributes.get("code_arm_required"):
|
||||
yield AlexaSecurityPanelController(self.hass, self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
@ -83,3 +83,17 @@ class AlexaBridgeUnreachableError(AlexaError):
|
||||
|
||||
namespace = "Alexa"
|
||||
error_type = "BRIDGE_UNREACHABLE"
|
||||
|
||||
|
||||
class AlexaSecurityPanelUnauthorizedError(AlexaError):
|
||||
"""Class to represent SecurityPanelController Unauthorized errors."""
|
||||
|
||||
namespace = "Alexa.SecurityPanelController"
|
||||
error_type = "UNAUTHORIZED"
|
||||
|
||||
|
||||
class AlexaSecurityPanelAuthorizationRequired(AlexaError):
|
||||
"""Class to represent SecurityPanelController AuthorizationRequired errors."""
|
||||
|
||||
namespace = "Alexa.SecurityPanelController"
|
||||
error_type = "AUTHORIZATION_REQUIRED"
|
||||
|
@ -9,6 +9,11 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE,
|
||||
STATE_ALARM_DISARMED,
|
||||
SERVICE_ALARM_ARM_AWAY,
|
||||
SERVICE_ALARM_ARM_HOME,
|
||||
SERVICE_ALARM_ARM_NIGHT,
|
||||
SERVICE_ALARM_DISARM,
|
||||
SERVICE_LOCK,
|
||||
SERVICE_MEDIA_NEXT_TRACK,
|
||||
SERVICE_MEDIA_PAUSE,
|
||||
@ -35,6 +40,8 @@ from .const import API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS,
|
||||
from .entities import async_get_entities
|
||||
from .errors import (
|
||||
AlexaInvalidValueError,
|
||||
AlexaSecurityPanelAuthorizationRequired,
|
||||
AlexaSecurityPanelUnauthorizedError,
|
||||
AlexaTempRangeError,
|
||||
AlexaUnsupportedThermostatModeError,
|
||||
)
|
||||
@ -849,3 +856,71 @@ async def async_api_adjust_power_level(hass, config, directive, context):
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.SecurityPanelController", "Arm"))
|
||||
async def async_api_arm(hass, config, directive, context):
|
||||
"""Process a Security Panel Arm request."""
|
||||
entity = directive.entity
|
||||
service = None
|
||||
arm_state = directive.payload["armState"]
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.state != STATE_ALARM_DISARMED:
|
||||
msg = "You must disarm the system before you can set the requested arm state."
|
||||
raise AlexaSecurityPanelAuthorizationRequired(msg)
|
||||
|
||||
if arm_state == "ARMED_AWAY":
|
||||
service = SERVICE_ALARM_ARM_AWAY
|
||||
if arm_state == "ARMED_STAY":
|
||||
service = SERVICE_ALARM_ARM_HOME
|
||||
if arm_state == "ARMED_NIGHT":
|
||||
service = SERVICE_ALARM_ARM_NIGHT
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
response = directive.response(
|
||||
name="Arm.Response", namespace="Alexa.SecurityPanelController"
|
||||
)
|
||||
|
||||
response.add_context_property(
|
||||
{
|
||||
"name": "armState",
|
||||
"namespace": "Alexa.SecurityPanelController",
|
||||
"value": arm_state,
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.SecurityPanelController", "Disarm"))
|
||||
async def async_api_disarm(hass, config, directive, context):
|
||||
"""Process a Security Panel Disarm request."""
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
payload = directive.payload
|
||||
if "authorization" in payload:
|
||||
value = payload["authorization"]["value"]
|
||||
if payload["authorization"]["type"] == "FOUR_DIGIT_PIN":
|
||||
data["code"] = value
|
||||
|
||||
if not await hass.services.async_call(
|
||||
entity.domain, SERVICE_ALARM_DISARM, data, blocking=True, context=context
|
||||
):
|
||||
msg = "Invalid Code"
|
||||
raise AlexaSecurityPanelUnauthorizedError(msg)
|
||||
|
||||
response = directive.response()
|
||||
response.add_context_property(
|
||||
{
|
||||
"name": "armState",
|
||||
"namespace": "Alexa.SecurityPanelController",
|
||||
"value": "DISARMED",
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
|
@ -4,5 +4,8 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
||||
"requirements": [],
|
||||
"dependencies": ["http"],
|
||||
"codeowners": ["@home-assistant/cloud"]
|
||||
"codeowners": [
|
||||
"@home-assistant/cloud",
|
||||
"@ochlocracy"
|
||||
]
|
||||
}
|
||||
|
@ -8,6 +8,11 @@ from homeassistant.const import (
|
||||
STATE_UNLOCKED,
|
||||
STATE_UNKNOWN,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
)
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.components.alexa import smart_home
|
||||
@ -527,3 +532,33 @@ async def test_temperature_sensor_climate(hass):
|
||||
properties.assert_equal(
|
||||
"Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"}
|
||||
)
|
||||
|
||||
|
||||
async def test_report_alarm_control_panel_state(hass):
|
||||
"""Test SecurityPanelController implements armState property."""
|
||||
hass.states.async_set("alarm_control_panel.armed_away", STATE_ALARM_ARMED_AWAY, {})
|
||||
hass.states.async_set(
|
||||
"alarm_control_panel.armed_custom_bypass", STATE_ALARM_ARMED_CUSTOM_BYPASS, {}
|
||||
)
|
||||
hass.states.async_set("alarm_control_panel.armed_home", STATE_ALARM_ARMED_HOME, {})
|
||||
hass.states.async_set(
|
||||
"alarm_control_panel.armed_night", STATE_ALARM_ARMED_NIGHT, {}
|
||||
)
|
||||
hass.states.async_set("alarm_control_panel.disarmed", STATE_ALARM_DISARMED, {})
|
||||
|
||||
properties = await reported_properties(hass, "alarm_control_panel.armed_away")
|
||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
|
||||
|
||||
properties = await reported_properties(
|
||||
hass, "alarm_control_panel.armed_custom_bypass"
|
||||
)
|
||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY")
|
||||
|
||||
properties = await reported_properties(hass, "alarm_control_panel.armed_home")
|
||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY")
|
||||
|
||||
properties = await reported_properties(hass, "alarm_control_panel.armed_night")
|
||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_NIGHT")
|
||||
|
||||
properties = await reported_properties(hass, "alarm_control_panel.disarmed")
|
||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED")
|
||||
|
@ -1306,3 +1306,127 @@ async def test_endpoint_bad_health(hass):
|
||||
properties.assert_equal(
|
||||
"Alexa.EndpointHealth", "connectivity", {"value": "UNREACHABLE"}
|
||||
)
|
||||
|
||||
|
||||
async def test_alarm_control_panel_disarmed(hass):
|
||||
"""Test alarm_control_panel discovery."""
|
||||
device = (
|
||||
"alarm_control_panel.test_1",
|
||||
"disarmed",
|
||||
{
|
||||
"friendly_name": "Test Alarm Control Panel 1",
|
||||
"code_arm_required": False,
|
||||
"code_format": "number",
|
||||
"code": "1234",
|
||||
},
|
||||
)
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
assert appliance["endpointId"] == "alarm_control_panel#test_1"
|
||||
assert appliance["displayCategories"][0] == "SECURITY_PANEL"
|
||||
assert appliance["friendlyName"] == "Test Alarm Control Panel 1"
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth"
|
||||
)
|
||||
security_panel_capability = get_capability(
|
||||
capabilities, "Alexa.SecurityPanelController"
|
||||
)
|
||||
assert security_panel_capability is not None
|
||||
configuration = security_panel_capability["configuration"]
|
||||
assert {"type": "FOUR_DIGIT_PIN"} in configuration["supportedAuthorizationTypes"]
|
||||
|
||||
properties = await reported_properties(hass, "alarm_control_panel#test_1")
|
||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED")
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.SecurityPanelController",
|
||||
"Arm",
|
||||
"alarm_control_panel#test_1",
|
||||
"alarm_control_panel.alarm_arm_home",
|
||||
hass,
|
||||
response_type="Arm.Response",
|
||||
payload={"armState": "ARMED_STAY"},
|
||||
)
|
||||
properties = ReportedProperties(msg["context"]["properties"])
|
||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY")
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.SecurityPanelController",
|
||||
"Arm",
|
||||
"alarm_control_panel#test_1",
|
||||
"alarm_control_panel.alarm_arm_away",
|
||||
hass,
|
||||
response_type="Arm.Response",
|
||||
payload={"armState": "ARMED_AWAY"},
|
||||
)
|
||||
properties = ReportedProperties(msg["context"]["properties"])
|
||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.SecurityPanelController",
|
||||
"Arm",
|
||||
"alarm_control_panel#test_1",
|
||||
"alarm_control_panel.alarm_arm_night",
|
||||
hass,
|
||||
response_type="Arm.Response",
|
||||
payload={"armState": "ARMED_NIGHT"},
|
||||
)
|
||||
properties = ReportedProperties(msg["context"]["properties"])
|
||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_NIGHT")
|
||||
|
||||
|
||||
async def test_alarm_control_panel_armed(hass):
|
||||
"""Test alarm_control_panel discovery."""
|
||||
device = (
|
||||
"alarm_control_panel.test_2",
|
||||
"armed_away",
|
||||
{
|
||||
"friendly_name": "Test Alarm Control Panel 2",
|
||||
"code_arm_required": False,
|
||||
"code_format": "FORMAT_NUMBER",
|
||||
"code": "1234",
|
||||
},
|
||||
)
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
assert appliance["endpointId"] == "alarm_control_panel#test_2"
|
||||
assert appliance["displayCategories"][0] == "SECURITY_PANEL"
|
||||
assert appliance["friendlyName"] == "Test Alarm Control Panel 2"
|
||||
assert_endpoint_capabilities(
|
||||
appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth"
|
||||
)
|
||||
|
||||
properties = await reported_properties(hass, "alarm_control_panel#test_2")
|
||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.SecurityPanelController",
|
||||
"Disarm",
|
||||
"alarm_control_panel#test_2",
|
||||
"alarm_control_panel.alarm_disarm",
|
||||
hass,
|
||||
payload={"authorization": {"type": "FOUR_DIGIT_PIN", "value": "1234"}},
|
||||
)
|
||||
assert call.data["code"] == "1234"
|
||||
properties = ReportedProperties(msg["context"]["properties"])
|
||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED")
|
||||
|
||||
msg = await assert_request_fails(
|
||||
"Alexa.SecurityPanelController",
|
||||
"Arm",
|
||||
"alarm_control_panel#test_2",
|
||||
"alarm_control_panel.alarm_arm_home",
|
||||
hass,
|
||||
payload={"armState": "ARMED_STAY"},
|
||||
)
|
||||
assert msg["event"]["payload"]["type"] == "AUTHORIZATION_REQUIRED"
|
||||
|
||||
|
||||
async def test_alarm_control_panel_code_arm_required(hass):
|
||||
"""Test alarm_control_panel with code_arm_required discovery."""
|
||||
device = (
|
||||
"alarm_control_panel.test_3",
|
||||
"disarmed",
|
||||
{"friendly_name": "Test Alarm Control Panel 3", "code_arm_required": True},
|
||||
)
|
||||
await discovery_test(device, hass, expected_endpoints=0)
|
||||
|
Loading…
x
Reference in New Issue
Block a user