Add Zigbee group binding to ZHA (#30433)

* initial group binding work
* add group cluster binding
This commit is contained in:
David F. Mulcahey 2020-01-04 16:58:51 -05:00 committed by Alexei Chetroi
parent 967fe89f6d
commit 6c89b6c5a2
3 changed files with 132 additions and 0 deletions

View file

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

View file

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

View file

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