Add device action support for ZHA (#26903)
* start implementing device actions * rename file * cleanup and add tests * fix docstrings * sort imports
This commit is contained in:
parent
f267b37105
commit
b1a9fa47ca
7 changed files with 313 additions and 76 deletions
|
@ -1,63 +1,67 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single configuration of ZHA is allowed."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Unable to connect to ZHA device."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"radio_type": "Radio Type",
|
||||
"usb_path": "USB Device Path"
|
||||
},
|
||||
"title": "ZHA"
|
||||
}
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single configuration of ZHA is allowed."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Unable to connect to ZHA device."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"radio_type": "Radio Type",
|
||||
"usb_path": "USB Device Path"
|
||||
},
|
||||
"title": "ZHA"
|
||||
}
|
||||
},
|
||||
"device_automation": {
|
||||
"trigger_subtype": {
|
||||
"both_buttons": "Both buttons",
|
||||
"button_1": "First button",
|
||||
"button_2": "Second button",
|
||||
"button_3": "Third button",
|
||||
"button_4": "Fourth button",
|
||||
"button_5": "Fifth button",
|
||||
"button_6": "Sixth button",
|
||||
"close": "Close",
|
||||
"dim_down": "Dim down",
|
||||
"dim_up": "Dim up",
|
||||
"face_1": "with face 1 activated",
|
||||
"face_2": "with face 2 activated",
|
||||
"face_3": "with face 3 activated",
|
||||
"face_4": "with face 4 activated",
|
||||
"face_5": "with face 5 activated",
|
||||
"face_6": "with face 6 activated",
|
||||
"face_any": "With any/specified face(s) activated",
|
||||
"left": "Left",
|
||||
"open": "Open",
|
||||
"right": "Right",
|
||||
"turn_off": "Turn off",
|
||||
"turn_on": "Turn on"
|
||||
},
|
||||
"trigger_type": {
|
||||
"device_dropped": "Device dropped",
|
||||
"device_flipped": "Device flipped \"{subtype}\"",
|
||||
"device_knocked": "Device knocked \"{subtype}\"",
|
||||
"device_rotated": "Device rotated \"{subtype}\"",
|
||||
"device_shaken": "Device shaken",
|
||||
"device_slid": "Device slid \"{subtype}\"",
|
||||
"device_tilted": "Device tilted",
|
||||
"remote_button_double_press": "\"{subtype}\" button double clicked",
|
||||
"remote_button_long_press": "\"{subtype}\" button continuously pressed",
|
||||
"remote_button_long_release": "\"{subtype}\" button released after long press",
|
||||
"remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked",
|
||||
"remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked",
|
||||
"remote_button_short_press": "\"{subtype}\" button pressed",
|
||||
"remote_button_short_release": "\"{subtype}\" button released",
|
||||
"remote_button_triple_press": "\"{subtype}\" button triple clicked"
|
||||
}
|
||||
"title": "ZHA"
|
||||
},
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"squawk": "Squawk",
|
||||
"warn": "Warn"
|
||||
},
|
||||
"trigger_subtype": {
|
||||
"both_buttons": "Both buttons",
|
||||
"button_1": "First button",
|
||||
"button_2": "Second button",
|
||||
"button_3": "Third button",
|
||||
"button_4": "Fourth button",
|
||||
"button_5": "Fifth button",
|
||||
"button_6": "Sixth button",
|
||||
"close": "Close",
|
||||
"dim_down": "Dim down",
|
||||
"dim_up": "Dim up",
|
||||
"face_1": "with face 1 activated",
|
||||
"face_2": "with face 2 activated",
|
||||
"face_3": "with face 3 activated",
|
||||
"face_4": "with face 4 activated",
|
||||
"face_5": "with face 5 activated",
|
||||
"face_6": "with face 6 activated",
|
||||
"face_any": "With any/specified face(s) activated",
|
||||
"left": "Left",
|
||||
"open": "Open",
|
||||
"right": "Right",
|
||||
"turn_off": "Turn off",
|
||||
"turn_on": "Turn on"
|
||||
},
|
||||
"trigger_type": {
|
||||
"device_dropped": "Device dropped",
|
||||
"device_flipped": "Device flipped \"{subtype}\"",
|
||||
"device_knocked": "Device knocked \"{subtype}\"",
|
||||
"device_rotated": "Device rotated \"{subtype}\"",
|
||||
"device_shaken": "Device shaken",
|
||||
"device_slid": "Device slid \"{subtype}\"",
|
||||
"device_tilted": "Device tilted",
|
||||
"remote_button_double_press": "\"{subtype}\" button double clicked",
|
||||
"remote_button_long_press": "\"{subtype}\" button continuously pressed",
|
||||
"remote_button_long_release": "\"{subtype}\" button released after long press",
|
||||
"remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked",
|
||||
"remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked",
|
||||
"remote_button_short_press": "\"{subtype}\" button pressed",
|
||||
"remote_button_short_release": "\"{subtype}\" button released",
|
||||
"remote_button_triple_press": "\"{subtype}\" button triple clicked"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,14 @@ import logging
|
|||
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DEFAULT_BAUDRATE, RadioType
|
||||
from .const import (
|
||||
CLUSTER_TYPE_IN,
|
||||
CLUSTER_TYPE_OUT,
|
||||
DATA_ZHA,
|
||||
DATA_ZHA_GATEWAY,
|
||||
DEFAULT_BAUDRATE,
|
||||
RadioType,
|
||||
)
|
||||
from .registries import BINDABLE_CLUSTERS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -132,6 +139,16 @@ def async_is_bindable_target(source_zha_device, target_zha_device):
|
|||
return False
|
||||
|
||||
|
||||
async def async_get_zha_device(hass, device_id):
|
||||
"""Get a ZHA device for the given device registry id."""
|
||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||
registry_device = device_registry.async_get(device_id)
|
||||
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
|
||||
ieee_address = list(list(registry_device.identifiers)[0])[1]
|
||||
ieee = convert_ieee(ieee_address)
|
||||
return zha_gateway.devices[ieee]
|
||||
|
||||
|
||||
class LogMixin:
|
||||
"""Log helper."""
|
||||
|
||||
|
|
92
homeassistant/components/zha/device_action.py
Normal file
92
homeassistant/components/zha/device_action.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
"""Provides device actions for ZHA devices."""
|
||||
from typing import List
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
|
||||
from . import DOMAIN
|
||||
from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN
|
||||
from .core.const import CHANNEL_IAS_WD
|
||||
from .core.helpers import async_get_zha_device
|
||||
|
||||
ACTION_SQUAWK = "squawk"
|
||||
ACTION_WARN = "warn"
|
||||
ATTR_DATA = "data"
|
||||
ATTR_IEEE = "ieee"
|
||||
CONF_ZHA_ACTION_TYPE = "zha_action_type"
|
||||
ZHA_ACTION_TYPE_SERVICE_CALL = "service_call"
|
||||
|
||||
ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
{vol.Required(CONF_DOMAIN): DOMAIN, vol.Required(CONF_TYPE): str}
|
||||
)
|
||||
|
||||
DEVICE_ACTIONS = {
|
||||
CHANNEL_IAS_WD: [
|
||||
{CONF_TYPE: ACTION_SQUAWK, CONF_DOMAIN: DOMAIN},
|
||||
{CONF_TYPE: ACTION_WARN, CONF_DOMAIN: DOMAIN},
|
||||
]
|
||||
}
|
||||
|
||||
DEVICE_ACTION_TYPES = {
|
||||
ACTION_SQUAWK: ZHA_ACTION_TYPE_SERVICE_CALL,
|
||||
ACTION_WARN: ZHA_ACTION_TYPE_SERVICE_CALL,
|
||||
}
|
||||
|
||||
SERVICE_NAMES = {
|
||||
ACTION_SQUAWK: SERVICE_WARNING_DEVICE_SQUAWK,
|
||||
ACTION_WARN: SERVICE_WARNING_DEVICE_WARN,
|
||||
}
|
||||
|
||||
|
||||
async def async_call_action_from_config(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
variables: TemplateVarsType,
|
||||
context: Context,
|
||||
) -> None:
|
||||
"""Perform an action based on configuration."""
|
||||
config = ACTION_SCHEMA(config)
|
||||
await ZHA_ACTION_TYPES[DEVICE_ACTION_TYPES[config[CONF_TYPE]]](
|
||||
hass, config, variables, context
|
||||
)
|
||||
|
||||
|
||||
async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
|
||||
"""List device actions."""
|
||||
zha_device = await async_get_zha_device(hass, device_id)
|
||||
actions = [
|
||||
action
|
||||
for channel in DEVICE_ACTIONS
|
||||
for action in DEVICE_ACTIONS[channel]
|
||||
if channel in zha_device.cluster_channels
|
||||
]
|
||||
for action in actions:
|
||||
action[CONF_DEVICE_ID] = device_id
|
||||
return actions
|
||||
|
||||
|
||||
async def _execute_service_based_action(
|
||||
hass: HomeAssistant,
|
||||
config: ACTION_SCHEMA,
|
||||
variables: TemplateVarsType,
|
||||
context: Context,
|
||||
) -> None:
|
||||
action_type = config[CONF_TYPE]
|
||||
service_name = SERVICE_NAMES[action_type]
|
||||
zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID])
|
||||
|
||||
service_action = {
|
||||
service.CONF_SERVICE: "{}.{}".format(DOMAIN, service_name),
|
||||
ATTR_DATA: {ATTR_IEEE: str(zha_device.ieee)},
|
||||
}
|
||||
|
||||
await service.async_call_from_config(
|
||||
hass, service_action, blocking=True, variables=variables, context=context
|
||||
)
|
||||
|
||||
|
||||
ZHA_ACTION_TYPES = {ZHA_ACTION_TYPE_SERVICE_CALL: _execute_service_based_action}
|
|
@ -9,8 +9,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF
|
|||
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
|
||||
|
||||
from . import DOMAIN
|
||||
from .core.const import DATA_ZHA, DATA_ZHA_GATEWAY
|
||||
from .core.helpers import convert_ieee
|
||||
from .core.helpers import async_get_zha_device
|
||||
|
||||
CONF_SUBTYPE = "subtype"
|
||||
DEVICE = "device"
|
||||
|
@ -26,7 +25,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
|
|||
"""Listen for state changes based on configuration."""
|
||||
config = TRIGGER_SCHEMA(config)
|
||||
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
|
||||
zha_device = await _async_get_zha_device(hass, config[CONF_DEVICE_ID])
|
||||
zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID])
|
||||
|
||||
if (
|
||||
zha_device.device_automation_triggers is None
|
||||
|
@ -52,7 +51,7 @@ async def async_get_triggers(hass, device_id):
|
|||
Make sure the device supports device automations and
|
||||
if it does return the trigger list.
|
||||
"""
|
||||
zha_device = await _async_get_zha_device(hass, device_id)
|
||||
zha_device = await async_get_zha_device(hass, device_id)
|
||||
|
||||
if not zha_device.device_automation_triggers:
|
||||
return
|
||||
|
@ -70,15 +69,3 @@ async def async_get_triggers(hass, device_id):
|
|||
)
|
||||
|
||||
return triggers
|
||||
|
||||
|
||||
async def _async_get_zha_device(hass, device_id):
|
||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||
registry_device = device_registry.async_get(device_id)
|
||||
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
|
||||
ieee_address = list(list(registry_device.identifiers)[0])[1]
|
||||
ieee = convert_ieee(ieee_address)
|
||||
zha_device = zha_gateway.devices[ieee]
|
||||
if not zha_device:
|
||||
raise InvalidDeviceAutomationConfig
|
||||
return zha_device
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
}
|
||||
},
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"squawk": "Squawk",
|
||||
"warn": "Warn"
|
||||
},
|
||||
"trigger_type": {
|
||||
"remote_button_short_press": "\"{subtype}\" button pressed",
|
||||
"remote_button_short_release": "\"{subtype}\" button released",
|
||||
|
|
133
tests/components/zha/test_device_action.py
Normal file
133
tests/components/zha/test_device_action.py
Normal file
|
@ -0,0 +1,133 @@
|
|||
"""The test for zha device automation actions."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
import homeassistant.components.automation as automation
|
||||
from homeassistant.components.device_automation import (
|
||||
_async_get_device_automations as async_get_device_automations,
|
||||
)
|
||||
from homeassistant.components.zha import DOMAIN
|
||||
from homeassistant.components.zha.core.const import CHANNEL_ON_OFF
|
||||
from homeassistant.helpers.device_registry import async_get_registry
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import async_enable_traffic, async_init_zigpy_device
|
||||
|
||||
from tests.common import async_mock_service, mock_coro
|
||||
|
||||
SHORT_PRESS = "remote_button_short_press"
|
||||
COMMAND = "command"
|
||||
COMMAND_SINGLE = "single"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def calls(hass):
|
||||
"""Track calls to a mock serivce."""
|
||||
return async_mock_service(hass, "zha", "warning_device_warn")
|
||||
|
||||
|
||||
async def test_get_actions(hass, config_entry, zha_gateway):
|
||||
"""Test we get the expected actions from a zha device."""
|
||||
from zigpy.zcl.clusters.general import Basic
|
||||
from zigpy.zcl.clusters.security import IasZone, IasWd
|
||||
|
||||
# create zigpy device
|
||||
zigpy_device = await async_init_zigpy_device(
|
||||
hass,
|
||||
[Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id],
|
||||
[],
|
||||
None,
|
||||
zha_gateway,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
|
||||
await hass.async_block_till_done()
|
||||
hass.config_entries._entries.append(config_entry)
|
||||
|
||||
zha_device = zha_gateway.get_device(zigpy_device.ieee)
|
||||
ieee_address = str(zha_device.ieee)
|
||||
|
||||
ha_device_registry = await async_get_registry(hass)
|
||||
reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set())
|
||||
|
||||
actions = await async_get_device_automations(hass, "action", reg_device.id)
|
||||
|
||||
expected_actions = [
|
||||
{"domain": DOMAIN, "type": "squawk", "device_id": reg_device.id},
|
||||
{"domain": DOMAIN, "type": "warn", "device_id": reg_device.id},
|
||||
]
|
||||
|
||||
assert actions == expected_actions
|
||||
|
||||
|
||||
async def test_action(hass, config_entry, zha_gateway, calls):
|
||||
"""Test for executing a zha device action."""
|
||||
|
||||
from zigpy.zcl.clusters.general import Basic, OnOff
|
||||
from zigpy.zcl.clusters.security import IasZone, IasWd
|
||||
from zigpy.zcl.foundation import Status
|
||||
|
||||
# create zigpy device
|
||||
zigpy_device = await async_init_zigpy_device(
|
||||
hass,
|
||||
[Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id],
|
||||
[OnOff.cluster_id],
|
||||
None,
|
||||
zha_gateway,
|
||||
)
|
||||
|
||||
zigpy_device.device_automation_triggers = {
|
||||
(SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}
|
||||
}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setup(config_entry, "switch")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.config_entries._entries.append(config_entry)
|
||||
|
||||
zha_device = zha_gateway.get_device(zigpy_device.ieee)
|
||||
ieee_address = str(zha_device.ieee)
|
||||
|
||||
ha_device_registry = await async_get_registry(hass)
|
||||
reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set())
|
||||
|
||||
# allow traffic to flow through the gateway and device
|
||||
await async_enable_traffic(hass, zha_gateway, [zha_device])
|
||||
|
||||
with patch(
|
||||
"zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS])
|
||||
):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: [
|
||||
{
|
||||
"trigger": {
|
||||
"device_id": reg_device.id,
|
||||
"domain": "zha",
|
||||
"platform": "device",
|
||||
"type": SHORT_PRESS,
|
||||
"subtype": SHORT_PRESS,
|
||||
},
|
||||
"action": {
|
||||
"domain": DOMAIN,
|
||||
"device_id": reg_device.id,
|
||||
"type": "warn",
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
on_off_channel = zha_device.cluster_channels[CHANNEL_ON_OFF]
|
||||
on_off_channel.zha_send_event(on_off_channel.cluster, COMMAND_SINGLE, [])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].domain == DOMAIN
|
||||
assert calls[0].service == "warning_device_warn"
|
||||
assert calls[0].data["ieee"] == ieee_address
|
|
@ -1,4 +1,4 @@
|
|||
"""ZHA device automation tests."""
|
||||
"""ZHA device automation trigger tests."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
Loading…
Add table
Add a link
Reference in a new issue