Add Zigbee group binding to ZHA (#30433)
* initial group binding work * add group cluster binding
This commit is contained in:
parent
967fe89f6d
commit
6c89b6c5a2
3 changed files with 132 additions and 0 deletions
|
@ -1,7 +1,9 @@
|
||||||
"""Web socket API for Zigbee Home Automation devices."""
|
"""Web socket API for Zigbee Home Automation devices."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import collections
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from zigpy.types.named import EUI64
|
from zigpy.types.named import EUI64
|
||||||
|
@ -31,6 +33,7 @@ from .core.const import (
|
||||||
ATTR_WARNING_DEVICE_STROBE,
|
ATTR_WARNING_DEVICE_STROBE,
|
||||||
ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE,
|
ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE,
|
||||||
ATTR_WARNING_DEVICE_STROBE_INTENSITY,
|
ATTR_WARNING_DEVICE_STROBE_INTENSITY,
|
||||||
|
BINDINGS,
|
||||||
CHANNEL_IAS_WD,
|
CHANNEL_IAS_WD,
|
||||||
CLUSTER_COMMAND_SERVER,
|
CLUSTER_COMMAND_SERVER,
|
||||||
CLUSTER_COMMANDS_CLIENT,
|
CLUSTER_COMMANDS_CLIENT,
|
||||||
|
@ -163,6 +166,8 @@ SERVICE_SCHEMAS = {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClusterBinding = collections.namedtuple("ClusterBinding", "id endpoint_id type name")
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.require_admin
|
@websocket_api.require_admin
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
|
@ -774,6 +779,64 @@ async def websocket_unbind_devices(hass, connection, msg):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_cluster_binding(value: Any) -> ClusterBinding:
|
||||||
|
"""Validate and transform a cluster binding."""
|
||||||
|
if not isinstance(value, collections.Mapping):
|
||||||
|
raise vol.Invalid("Not a cluster binding")
|
||||||
|
try:
|
||||||
|
cluster_binding = ClusterBinding(
|
||||||
|
name=value["name"],
|
||||||
|
type=value["type"],
|
||||||
|
id=value["id"],
|
||||||
|
endpoint_id=value["endpoint_id"],
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
raise vol.Invalid("Not a cluster binding")
|
||||||
|
|
||||||
|
return cluster_binding
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.async_response
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required(TYPE): "zha/groups/bind",
|
||||||
|
vol.Required(ATTR_SOURCE_IEEE): EUI64.convert,
|
||||||
|
vol.Required(GROUP_ID): cv.positive_int,
|
||||||
|
vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
async def websocket_bind_group(hass, connection, msg):
|
||||||
|
"""Directly bind a device to a group."""
|
||||||
|
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
|
||||||
|
source_ieee = msg[ATTR_SOURCE_IEEE]
|
||||||
|
group_id = msg[GROUP_ID]
|
||||||
|
bindings = msg[BINDINGS]
|
||||||
|
source_device = zha_gateway.get_device(source_ieee)
|
||||||
|
|
||||||
|
await source_device.async_bind_to_group(group_id, bindings)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.async_response
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required(TYPE): "zha/groups/unbind",
|
||||||
|
vol.Required(ATTR_SOURCE_IEEE): EUI64.convert,
|
||||||
|
vol.Required(GROUP_ID): cv.positive_int,
|
||||||
|
vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
async def websocket_unbind_group(hass, connection, msg):
|
||||||
|
"""Unbind a device from a group."""
|
||||||
|
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
|
||||||
|
source_ieee = msg[ATTR_SOURCE_IEEE]
|
||||||
|
group_id = msg[GROUP_ID]
|
||||||
|
bindings = msg[BINDINGS]
|
||||||
|
source_device = zha_gateway.get_device(source_ieee)
|
||||||
|
await source_device.async_unbind_from_group(group_id, bindings)
|
||||||
|
|
||||||
|
|
||||||
async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operation):
|
async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operation):
|
||||||
"""Create or remove a direct zigbee binding between 2 devices."""
|
"""Create or remove a direct zigbee binding between 2 devices."""
|
||||||
|
|
||||||
|
@ -1082,6 +1145,8 @@ def async_load_api(hass):
|
||||||
websocket_api.async_register_command(hass, websocket_remove_groups)
|
websocket_api.async_register_command(hass, websocket_remove_groups)
|
||||||
websocket_api.async_register_command(hass, websocket_add_group_members)
|
websocket_api.async_register_command(hass, websocket_add_group_members)
|
||||||
websocket_api.async_register_command(hass, websocket_remove_group_members)
|
websocket_api.async_register_command(hass, websocket_remove_group_members)
|
||||||
|
websocket_api.async_register_command(hass, websocket_bind_group)
|
||||||
|
websocket_api.async_register_command(hass, websocket_unbind_group)
|
||||||
websocket_api.async_register_command(hass, websocket_reconfigure_node)
|
websocket_api.async_register_command(hass, websocket_reconfigure_node)
|
||||||
websocket_api.async_register_command(hass, websocket_device_clusters)
|
websocket_api.async_register_command(hass, websocket_device_clusters)
|
||||||
websocket_api.async_register_command(hass, websocket_device_cluster_attributes)
|
websocket_api.async_register_command(hass, websocket_device_cluster_attributes)
|
||||||
|
|
|
@ -42,6 +42,7 @@ ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE = "duty_cycle"
|
||||||
ATTR_WARNING_DEVICE_STROBE_INTENSITY = "intensity"
|
ATTR_WARNING_DEVICE_STROBE_INTENSITY = "intensity"
|
||||||
|
|
||||||
BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000]
|
BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000]
|
||||||
|
BINDINGS = "bindings"
|
||||||
|
|
||||||
CHANNEL_ACCELEROMETER = "accelerometer"
|
CHANNEL_ACCELEROMETER = "accelerometer"
|
||||||
CHANNEL_ATTRIBUTE = "attribute"
|
CHANNEL_ATTRIBUTE = "attribute"
|
||||||
|
|
|
@ -10,10 +10,12 @@ from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from zigpy import types
|
||||||
import zigpy.exceptions
|
import zigpy.exceptions
|
||||||
from zigpy.profiles import zha, zll
|
from zigpy.profiles import zha, zll
|
||||||
import zigpy.quirks
|
import zigpy.quirks
|
||||||
from zigpy.zcl.clusters.general import Groups
|
from zigpy.zcl.clusters.general import Groups
|
||||||
|
import zigpy.zdo.types as zdo_types
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
@ -526,6 +528,70 @@ class ZHADevice(LogMixin):
|
||||||
"""Remove this device from the provided zigbee group."""
|
"""Remove this device from the provided zigbee group."""
|
||||||
await self._zigpy_device.remove_from_group(group_id)
|
await self._zigpy_device.remove_from_group(group_id)
|
||||||
|
|
||||||
|
async def async_bind_to_group(self, group_id, cluster_bindings):
|
||||||
|
"""Directly bind this device to a group for the given clusters."""
|
||||||
|
await self._async_group_binding_operation(
|
||||||
|
group_id, zdo_types.ZDOCmd.Bind_req, cluster_bindings
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_unbind_from_group(self, group_id, cluster_bindings):
|
||||||
|
"""Unbind this device from a group for the given clusters."""
|
||||||
|
await self._async_group_binding_operation(
|
||||||
|
group_id, zdo_types.ZDOCmd.Unbind_req, cluster_bindings
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_group_binding_operation(
|
||||||
|
self, group_id, operation, cluster_bindings
|
||||||
|
):
|
||||||
|
"""Create or remove a direct zigbee binding between a device and a group."""
|
||||||
|
|
||||||
|
zdo = self._zigpy_device.zdo
|
||||||
|
op_msg = "0x%04x: %s %s, ep: %s, cluster: %s to group: 0x%04x"
|
||||||
|
destination_address = zdo_types.MultiAddress()
|
||||||
|
destination_address.addrmode = types.uint8_t(1)
|
||||||
|
destination_address.nwk = types.uint16_t(group_id)
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
for cluster_binding in cluster_bindings:
|
||||||
|
if cluster_binding.endpoint_id == 0:
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
cluster_binding.id
|
||||||
|
in self._zigpy_device.endpoints[
|
||||||
|
cluster_binding.endpoint_id
|
||||||
|
].out_clusters
|
||||||
|
):
|
||||||
|
op_params = (
|
||||||
|
self.nwk,
|
||||||
|
operation.name,
|
||||||
|
str(self.ieee),
|
||||||
|
cluster_binding.endpoint_id,
|
||||||
|
cluster_binding.id,
|
||||||
|
group_id,
|
||||||
|
)
|
||||||
|
zdo.debug("processing " + op_msg, *op_params)
|
||||||
|
tasks.append(
|
||||||
|
(
|
||||||
|
zdo.request(
|
||||||
|
operation,
|
||||||
|
self.ieee,
|
||||||
|
cluster_binding.endpoint_id,
|
||||||
|
cluster_binding.id,
|
||||||
|
destination_address,
|
||||||
|
),
|
||||||
|
op_msg,
|
||||||
|
op_params,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
res = await asyncio.gather(*(t[0] for t in tasks), return_exceptions=True)
|
||||||
|
for outcome, log_msg in zip(res, tasks):
|
||||||
|
if isinstance(outcome, Exception):
|
||||||
|
fmt = log_msg[1] + " failed: %s"
|
||||||
|
else:
|
||||||
|
fmt = log_msg[1] + " completed: %s"
|
||||||
|
zdo.debug(fmt, *(log_msg[2] + (outcome,)))
|
||||||
|
|
||||||
def log(self, level, msg, *args):
|
def log(self, level, msg, *args):
|
||||||
"""Log a message."""
|
"""Log a message."""
|
||||||
msg = f"[%s](%s): {msg}"
|
msg = f"[%s](%s): {msg}"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue