Add device action support to the alarm_control_panel integration (#27616)

* Add device action support to the alarm_control_panel integration

* Improve tests
This commit is contained in:
Erik Montnemery 2019-10-17 06:34:56 +02:00 committed by Paulus Schoutsen
parent 6ffc520b1c
commit 43c85c0549
6 changed files with 623 additions and 0 deletions

View file

@ -0,0 +1,126 @@
"""Provides device automations for Alarm control panel."""
from typing import Optional, List
import voluptuous as vol
from homeassistant.const import (
ATTR_CODE,
ATTR_ENTITY_ID,
CONF_CODE,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_TYPE,
SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_HOME,
SERVICE_ALARM_ARM_NIGHT,
SERVICE_ALARM_DISARM,
SERVICE_ALARM_TRIGGER,
)
from homeassistant.core import HomeAssistant, Context
from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
from . import ATTR_CODE_ARM_REQUIRED, DOMAIN
ACTION_TYPES = {"arm_away", "arm_home", "arm_night", "disarm", "trigger"}
ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In(ACTION_TYPES),
vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
vol.Optional(CONF_CODE): cv.string,
}
)
async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
"""List device actions for Alarm control panel devices."""
registry = await entity_registry.async_get_registry(hass)
actions = []
# Get all the integrations entities for this device
for entry in entity_registry.async_entries_for_device(registry, device_id):
if entry.domain != DOMAIN:
continue
# Add actions for each entity that belongs to this integration
actions.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "arm_away",
}
)
actions.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "arm_home",
}
)
actions.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "arm_night",
}
)
actions.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "disarm",
}
)
actions.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "trigger",
}
)
return actions
async def async_call_action_from_config(
hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
) -> None:
"""Execute a device action."""
config = ACTION_SCHEMA(config)
service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
if CONF_CODE in config:
service_data[ATTR_CODE] = config[CONF_CODE]
if config[CONF_TYPE] == "arm_away":
service = SERVICE_ALARM_ARM_AWAY
elif config[CONF_TYPE] == "arm_home":
service = SERVICE_ALARM_ARM_HOME
elif config[CONF_TYPE] == "arm_night":
service = SERVICE_ALARM_ARM_NIGHT
elif config[CONF_TYPE] == "disarm":
service = SERVICE_ALARM_DISARM
elif config[CONF_TYPE] == "trigger":
service = SERVICE_ALARM_TRIGGER
await hass.services.async_call(
DOMAIN, service, service_data, blocking=True, context=context
)
async def async_get_action_capabilities(hass, config):
"""List action capabilities."""
state = hass.states.get(config[CONF_ENTITY_ID])
code_required = state.attributes.get(ATTR_CODE_ARM_REQUIRED) if state else False
if config[CONF_TYPE] == "trigger" or (
config[CONF_TYPE] != "disarm" and not code_required
):
return {}
return {"extra_fields": vol.Schema({vol.Optional(CONF_CODE): str})}

View file

@ -0,0 +1,11 @@
{
"device_automation": {
"action_type": {
"arm_away": "Arm {entity_name} away",
"arm_home": "Arm {entity_name} home",
"arm_night": "Arm {entity_name} night",
"disarm": "Disarm {entity_name}",
"trigger": "Trigger {entity_name}"
}
}
}

View file

@ -59,6 +59,9 @@ async def async_setup(hass, config):
hass.components.websocket_api.async_register_command(
websocket_device_automation_list_triggers
)
hass.components.websocket_api.async_register_command(
websocket_device_automation_get_action_capabilities
)
hass.components.websocket_api.async_register_command(
websocket_device_automation_get_condition_capabilities
)
@ -209,6 +212,22 @@ async def websocket_device_automation_list_triggers(hass, connection, msg):
connection.send_result(msg["id"], triggers)
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "device_automation/action/capabilities",
vol.Required("action"): dict,
}
)
async def websocket_device_automation_get_action_capabilities(hass, connection, msg):
"""Handle request for device action capabilities."""
action = msg["action"]
capabilities = await _async_get_device_automation_capabilities(
hass, "action", action
)
connection.send_result(msg["id"], capabilities)
@websocket_api.async_response
@websocket_api.websocket_command(
{

View file

@ -0,0 +1,274 @@
"""The tests for Alarm control panel device actions."""
import pytest
from homeassistant.components.alarm_control_panel import DOMAIN
from homeassistant.const import (
CONF_PLATFORM,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
STATE_UNKNOWN,
)
from homeassistant.setup import async_setup_component
import homeassistant.components.automation as automation
from homeassistant.helpers import device_registry
from tests.common import (
MockConfigEntry,
assert_lists_same,
mock_device_registry,
mock_registry,
async_get_device_automations,
async_get_device_automation_capabilities,
)
@pytest.fixture
def device_reg(hass):
"""Return an empty, loaded, registry."""
return mock_device_registry(hass)
@pytest.fixture
def entity_reg(hass):
"""Return an empty, loaded, registry."""
return mock_registry(hass)
async def test_get_actions(hass, device_reg, entity_reg):
"""Test we get the expected actions from a alarm_control_panel."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
expected_actions = [
{
"domain": DOMAIN,
"type": "arm_away",
"device_id": device_entry.id,
"entity_id": "alarm_control_panel.test_5678",
},
{
"domain": DOMAIN,
"type": "arm_home",
"device_id": device_entry.id,
"entity_id": "alarm_control_panel.test_5678",
},
{
"domain": DOMAIN,
"type": "arm_night",
"device_id": device_entry.id,
"entity_id": "alarm_control_panel.test_5678",
},
{
"domain": DOMAIN,
"type": "disarm",
"device_id": device_entry.id,
"entity_id": "alarm_control_panel.test_5678",
},
{
"domain": DOMAIN,
"type": "trigger",
"device_id": device_entry.id,
"entity_id": "alarm_control_panel.test_5678",
},
]
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert_lists_same(actions, expected_actions)
async def test_get_action_capabilities(hass, device_reg, entity_reg):
"""Test we get the expected capabilities from a sensor trigger."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_reg.async_get_or_create(
DOMAIN,
"test",
platform.ENTITIES["no_arm_code"].unique_id,
device_id=device_entry.id,
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
expected_capabilities = {
"arm_away": {"extra_fields": []},
"arm_home": {"extra_fields": []},
"arm_night": {"extra_fields": []},
"disarm": {
"extra_fields": [{"name": "code", "optional": True, "type": "string"}]
},
"trigger": {"extra_fields": []},
}
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert len(actions) == 5
for action in actions:
capabilities = await async_get_device_automation_capabilities(
hass, "action", action
)
assert capabilities == expected_capabilities[action["type"]]
async def test_get_action_capabilities_arm_code(hass, device_reg, entity_reg):
"""Test we get the expected capabilities from a sensor trigger."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_reg.async_get_or_create(
DOMAIN,
"test",
platform.ENTITIES["arm_code"].unique_id,
device_id=device_entry.id,
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
expected_capabilities = {
"arm_away": {
"extra_fields": [{"name": "code", "optional": True, "type": "string"}]
},
"arm_home": {
"extra_fields": [{"name": "code", "optional": True, "type": "string"}]
},
"arm_night": {
"extra_fields": [{"name": "code", "optional": True, "type": "string"}]
},
"disarm": {
"extra_fields": [{"name": "code", "optional": True, "type": "string"}]
},
"trigger": {"extra_fields": []},
}
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert len(actions) == 5
for action in actions:
capabilities = await async_get_device_automation_capabilities(
hass, "action", action
)
assert capabilities == expected_capabilities[action["type"]]
async def test_action(hass):
"""Test for turn_on and turn_off actions."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": "event",
"event_type": "test_event_arm_away",
},
"action": {
"domain": DOMAIN,
"device_id": "abcdefgh",
"entity_id": "alarm_control_panel.alarm_no_arm_code",
"type": "arm_away",
},
},
{
"trigger": {
"platform": "event",
"event_type": "test_event_arm_home",
},
"action": {
"domain": DOMAIN,
"device_id": "abcdefgh",
"entity_id": "alarm_control_panel.alarm_no_arm_code",
"type": "arm_home",
},
},
{
"trigger": {
"platform": "event",
"event_type": "test_event_arm_night",
},
"action": {
"domain": DOMAIN,
"device_id": "abcdefgh",
"entity_id": "alarm_control_panel.alarm_no_arm_code",
"type": "arm_night",
},
},
{
"trigger": {"platform": "event", "event_type": "test_event_disarm"},
"action": {
"domain": DOMAIN,
"device_id": "abcdefgh",
"entity_id": "alarm_control_panel.alarm_no_arm_code",
"type": "disarm",
"code": "1234",
},
},
{
"trigger": {
"platform": "event",
"event_type": "test_event_trigger",
},
"action": {
"domain": DOMAIN,
"device_id": "abcdefgh",
"entity_id": "alarm_control_panel.alarm_no_arm_code",
"type": "trigger",
},
},
]
},
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
assert (
hass.states.get("alarm_control_panel.alarm_no_arm_code").state == STATE_UNKNOWN
)
hass.bus.async_fire("test_event_arm_away")
await hass.async_block_till_done()
assert (
hass.states.get("alarm_control_panel.alarm_no_arm_code").state
== STATE_ALARM_ARMED_AWAY
)
hass.bus.async_fire("test_event_arm_home")
await hass.async_block_till_done()
assert (
hass.states.get("alarm_control_panel.alarm_no_arm_code").state
== STATE_ALARM_ARMED_HOME
)
hass.bus.async_fire("test_event_arm_night")
await hass.async_block_till_done()
assert (
hass.states.get("alarm_control_panel.alarm_no_arm_code").state
== STATE_ALARM_ARMED_NIGHT
)
hass.bus.async_fire("test_event_disarm")
await hass.async_block_till_done()
assert (
hass.states.get("alarm_control_panel.alarm_no_arm_code").state
== STATE_ALARM_DISARMED
)
hass.bus.async_fire("test_event_trigger")
await hass.async_block_till_done()
assert (
hass.states.get("alarm_control_panel.alarm_no_arm_code").state
== STATE_ALARM_TRIGGERED
)

View file

@ -170,6 +170,106 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r
assert _same_lists(triggers, expected_triggers)
async def test_websocket_get_action_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
"""Test we get the expected action capabilities for an alarm through websocket."""
await async_setup_component(hass, "device_automation", {})
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_reg.async_get_or_create(
"alarm_control_panel", "test", "5678", device_id=device_entry.id
)
expected_capabilities = {
"arm_away": {"extra_fields": []},
"arm_home": {"extra_fields": []},
"arm_night": {"extra_fields": []},
"disarm": {
"extra_fields": [{"name": "code", "optional": True, "type": "string"}]
},
"trigger": {"extra_fields": []},
}
client = await hass_ws_client(hass)
await client.send_json(
{"id": 1, "type": "device_automation/action/list", "device_id": device_entry.id}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
actions = msg["result"]
id = 2
assert len(actions) == 5
for action in actions:
await client.send_json(
{
"id": id,
"type": "device_automation/action/capabilities",
"action": action,
}
)
msg = await client.receive_json()
assert msg["id"] == id
assert msg["type"] == TYPE_RESULT
assert msg["success"]
capabilities = msg["result"]
assert capabilities == expected_capabilities[action["type"]]
id = id + 1
async def test_websocket_get_bad_action_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
"""Test we get no action capabilities for a non existing domain."""
await async_setup_component(hass, "device_automation", {})
expected_capabilities = {}
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "device_automation/action/capabilities",
"action": {"domain": "beer"},
}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
capabilities = msg["result"]
assert capabilities == expected_capabilities
async def test_websocket_get_no_action_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
"""Test we get no action capabilities for a domain with no device action capabilities."""
await async_setup_component(hass, "device_automation", {})
expected_capabilities = {}
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "device_automation/action/capabilities",
"action": {"domain": "deconz"},
}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
capabilities = msg["result"]
assert capabilities == expected_capabilities
async def test_websocket_get_condition_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
@ -204,6 +304,7 @@ async def test_websocket_get_condition_capabilities(
conditions = msg["result"]
id = 2
assert len(conditions) == 2
for condition in conditions:
await client.send_json(
{
@ -301,6 +402,7 @@ async def test_websocket_get_trigger_capabilities(
triggers = msg["result"]
id = 2
assert len(triggers) == 2
for trigger in triggers:
await client.send_json(
{

View file

@ -0,0 +1,91 @@
"""
Provide a mock alarm_control_panel platform.
Call init before using it in your tests to ensure clean test data.
"""
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
from tests.common import MockEntity
ENTITIES = {}
def init(empty=False):
"""Initialize the platform with entities."""
global ENTITIES
ENTITIES = (
{}
if empty
else {
"arm_code": MockAlarm(
name=f"Alarm arm code",
code_arm_required=True,
unique_id="unique_arm_code",
),
"no_arm_code": MockAlarm(
name=f"Alarm no arm code",
code_arm_required=False,
unique_id="unique_no_arm_code",
),
}
)
async def async_setup_platform(
hass, config, async_add_entities_callback, discovery_info=None
):
"""Return mock entities."""
async_add_entities_callback(list(ENTITIES.values()))
class MockAlarm(MockEntity, AlarmControlPanel):
"""Mock Alarm control panel class."""
def __init__(self, **values):
"""Init the Mock Alarm Control Panel."""
self._state = None
MockEntity.__init__(self, **values)
@property
def code_arm_required(self):
"""Whether the code is required for arm actions."""
return self._handle("code_arm_required")
@property
def state(self):
"""Return the state of the device."""
return self._state
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._state = STATE_ALARM_ARMED_AWAY
self.async_write_ha_state()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._state = STATE_ALARM_ARMED_HOME
self.async_write_ha_state()
def alarm_arm_night(self, code=None):
"""Send arm night command."""
self._state = STATE_ALARM_ARMED_NIGHT
self.async_write_ha_state()
def alarm_disarm(self, code=None):
"""Send disarm command."""
if code == "1234":
self._state = STATE_ALARM_DISARMED
self.async_write_ha_state()
def alarm_trigger(self, code=None):
"""Send alarm trigger command."""
self._state = STATE_ALARM_TRIGGERED
self.async_write_ha_state()