Add direct binding for remotes and lights for ZHA (#21498)
* cluster matching and binding apis implement binding callback fix loop fix loops * review comments * use any because it is clearerpull/21542/head
parent
5ce4fe65b2
commit
82bdd9568d
|
@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
|
|||
https://home-assistant.io/components/zha/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -14,6 +15,7 @@ from .core.const import (
|
|||
DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE,
|
||||
ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT,
|
||||
CLIENT_COMMANDS, SERVER_COMMANDS, SERVER, NAME, ATTR_ENDPOINT_ID)
|
||||
from .core.helpers import get_matched_clusters, async_is_bindable_target
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -26,11 +28,18 @@ DEVICE_INFO = 'device_info'
|
|||
ATTR_DURATION = 'duration'
|
||||
ATTR_IEEE_ADDRESS = 'ieee_address'
|
||||
ATTR_IEEE = 'ieee'
|
||||
ATTR_SOURCE_IEEE = 'source_ieee'
|
||||
ATTR_TARGET_IEEE = 'target_ieee'
|
||||
BIND_REQUEST = 0x0021
|
||||
UNBIND_REQUEST = 0x0022
|
||||
|
||||
SERVICE_PERMIT = 'permit'
|
||||
SERVICE_REMOVE = 'remove'
|
||||
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = 'set_zigbee_cluster_attribute'
|
||||
SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = 'issue_zigbee_cluster_command'
|
||||
SERVICE_DIRECT_ZIGBEE_BIND = 'issue_direct_zigbee_bind'
|
||||
SERVICE_DIRECT_ZIGBEE_UNBIND = 'issue_direct_zigbee_unbind'
|
||||
SERVICE_ZIGBEE_BIND = 'service_zigbee_bind'
|
||||
IEEE_SERVICE = 'ieee_based_service'
|
||||
|
||||
SERVICE_SCHEMAS = {
|
||||
|
@ -110,6 +119,26 @@ SCHEMA_WS_CLUSTER_COMMANDS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
|||
vol.Required(ATTR_CLUSTER_TYPE): str
|
||||
})
|
||||
|
||||
WS_BIND_DEVICE = 'zha/devices/bind'
|
||||
SCHEMA_WS_BIND_DEVICE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required(TYPE): WS_BIND_DEVICE,
|
||||
vol.Required(ATTR_SOURCE_IEEE): str,
|
||||
vol.Required(ATTR_TARGET_IEEE): str
|
||||
})
|
||||
|
||||
WS_UNBIND_DEVICE = 'zha/devices/unbind'
|
||||
SCHEMA_WS_UNBIND_DEVICE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required(TYPE): WS_UNBIND_DEVICE,
|
||||
vol.Required(ATTR_SOURCE_IEEE): str,
|
||||
vol.Required(ATTR_TARGET_IEEE): str
|
||||
})
|
||||
|
||||
WS_BINDABLE_DEVICES = 'zha/devices/bindable'
|
||||
SCHEMA_WS_BINDABLE_DEVICES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required(TYPE): WS_BINDABLE_DEVICES,
|
||||
vol.Required(ATTR_IEEE): str
|
||||
})
|
||||
|
||||
|
||||
def async_load_api(hass, application_controller, zha_gateway):
|
||||
"""Set up the web socket API."""
|
||||
|
@ -244,6 +273,103 @@ def async_load_api(hass, application_controller, zha_gateway):
|
|||
SCHEMA_WS_RECONFIGURE_NODE
|
||||
)
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_get_bindable_devices(hass, connection, msg):
|
||||
"""Directly bind devices."""
|
||||
source_ieee = msg[ATTR_IEEE]
|
||||
source_device = zha_gateway.get_device(source_ieee)
|
||||
devices = [
|
||||
{
|
||||
**device.device_info
|
||||
} for device in zha_gateway.devices.values() if
|
||||
async_is_bindable_target(source_device, device)
|
||||
]
|
||||
|
||||
_LOGGER.debug("Get bindable devices: %s %s",
|
||||
"{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee),
|
||||
"{}: [{}]".format('bindable devices:', devices)
|
||||
)
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
devices
|
||||
))
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_BINDABLE_DEVICES, websocket_get_bindable_devices,
|
||||
SCHEMA_WS_BINDABLE_DEVICES
|
||||
)
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_bind_devices(hass, connection, msg):
|
||||
"""Directly bind devices."""
|
||||
source_ieee = msg[ATTR_SOURCE_IEEE]
|
||||
target_ieee = msg[ATTR_TARGET_IEEE]
|
||||
await async_binding_operation(
|
||||
source_ieee, target_ieee, BIND_REQUEST)
|
||||
_LOGGER.info("Issue bind devices: %s %s",
|
||||
"{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee),
|
||||
"{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee)
|
||||
)
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_BIND_DEVICE, websocket_bind_devices,
|
||||
SCHEMA_WS_BIND_DEVICE
|
||||
)
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_unbind_devices(hass, connection, msg):
|
||||
"""Remove a direct binding between devices."""
|
||||
source_ieee = msg[ATTR_SOURCE_IEEE]
|
||||
target_ieee = msg[ATTR_TARGET_IEEE]
|
||||
await async_binding_operation(
|
||||
source_ieee, target_ieee, UNBIND_REQUEST)
|
||||
_LOGGER.info("Issue unbind devices: %s %s",
|
||||
"{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee),
|
||||
"{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee)
|
||||
)
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_UNBIND_DEVICE, websocket_unbind_devices,
|
||||
SCHEMA_WS_UNBIND_DEVICE
|
||||
)
|
||||
|
||||
async def async_binding_operation(source_ieee, target_ieee,
|
||||
operation):
|
||||
"""Create or remove a direct zigbee binding between 2 devices."""
|
||||
from zigpy.zdo import types as zdo_types
|
||||
source_device = zha_gateway.get_device(source_ieee)
|
||||
target_device = zha_gateway.get_device(target_ieee)
|
||||
|
||||
clusters_to_bind = await get_matched_clusters(source_device,
|
||||
target_device)
|
||||
|
||||
bind_tasks = []
|
||||
for cluster_pair in clusters_to_bind:
|
||||
destination_address = zdo_types.MultiAddress()
|
||||
destination_address.addrmode = 3
|
||||
destination_address.ieee = target_device.ieee
|
||||
destination_address.endpoint = \
|
||||
cluster_pair.target_cluster.endpoint.endpoint_id
|
||||
|
||||
zdo = cluster_pair.source_cluster.endpoint.device.zdo
|
||||
|
||||
_LOGGER.debug("processing binding operation for: %s %s %s",
|
||||
"{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee),
|
||||
"{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee),
|
||||
"{}: {}".format(
|
||||
'cluster',
|
||||
cluster_pair.source_cluster.cluster_id)
|
||||
)
|
||||
bind_tasks.append(zdo.request(
|
||||
operation,
|
||||
source_device.ieee,
|
||||
cluster_pair.source_cluster.endpoint.endpoint_id,
|
||||
cluster_pair.source_cluster.cluster_id,
|
||||
destination_address
|
||||
))
|
||||
await asyncio.gather(*bind_tasks)
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_device_clusters(hass, connection, msg):
|
||||
"""Return a list of device clusters."""
|
||||
|
|
|
@ -114,6 +114,7 @@ CUSTOM_CLUSTER_MAPPINGS = {}
|
|||
COMPONENT_CLUSTERS = {}
|
||||
EVENT_RELAY_CLUSTERS = []
|
||||
NO_SENSOR_CLUSTERS = []
|
||||
BINDABLE_CLUSTERS = []
|
||||
|
||||
REPORT_CONFIG_MAX_INT = 900
|
||||
REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800
|
||||
|
|
|
@ -242,6 +242,18 @@ class ZHADevice:
|
|||
if ep_id != 0
|
||||
}
|
||||
|
||||
@callback
|
||||
def async_get_zha_clusters(self):
|
||||
"""Get zigbee home automation clusters for this device."""
|
||||
from zigpy.profiles.zha import PROFILE_ID
|
||||
return {
|
||||
ep_id: {
|
||||
IN: endpoint.in_clusters,
|
||||
OUT: endpoint.out_clusters
|
||||
} for (ep_id, endpoint) in self._zigpy_device.endpoints.items()
|
||||
if ep_id != 0 and endpoint.profile_id == PROFILE_ID
|
||||
}
|
||||
|
||||
@callback
|
||||
def async_get_cluster(self, endpoint_id, cluster_id, cluster_type=IN):
|
||||
"""Get zigbee cluster from this entity."""
|
||||
|
|
|
@ -18,11 +18,11 @@ from .const import (
|
|||
ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
|
||||
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY,
|
||||
TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
|
||||
GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS, UNKNOWN,
|
||||
OPENING, ZONE, OCCUPANCY, CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE,
|
||||
GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS, UNKNOWN, OPENING, ZONE,
|
||||
OCCUPANCY, CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE,
|
||||
REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT,
|
||||
REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, SIGNAL_REMOVE, NO_SENSOR_CLUSTERS,
|
||||
POWER_CONFIGURATION_CHANNEL)
|
||||
REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, SIGNAL_REMOVE,
|
||||
NO_SENSOR_CLUSTERS, POWER_CONFIGURATION_CHANNEL, BINDABLE_CLUSTERS)
|
||||
from .device import ZHADevice, DeviceStatus
|
||||
from ..device_entity import ZhaDeviceEntity
|
||||
from .channels import (
|
||||
|
@ -450,6 +450,8 @@ def establish_device_mappings():
|
|||
NO_SENSOR_CLUSTERS.append(zcl.clusters.general.Basic.cluster_id)
|
||||
NO_SENSOR_CLUSTERS.append(
|
||||
zcl.clusters.general.PowerConfiguration.cluster_id)
|
||||
BINDABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id)
|
||||
BINDABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
|
||||
|
||||
DEVICE_CLASS[zha.PROFILE_ID].update({
|
||||
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
|
||||
|
|
|
@ -5,14 +5,19 @@ For more details about this component, please refer to the documentation at
|
|||
https://home-assistant.io/components/zha/
|
||||
"""
|
||||
import asyncio
|
||||
import collections
|
||||
import logging
|
||||
from concurrent.futures import TimeoutError as Timeout
|
||||
from homeassistant.core import callback
|
||||
from .const import (
|
||||
DEFAULT_BAUDRATE, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT,
|
||||
REPORT_CONFIG_RPT_CHANGE, RadioType)
|
||||
REPORT_CONFIG_RPT_CHANGE, RadioType, IN, OUT, BINDABLE_CLUSTERS)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ClusterPair = collections.namedtuple(
|
||||
'ClusterPair', 'source_cluster target_cluster')
|
||||
|
||||
|
||||
async def safe_read(cluster, attributes, allow_cache=True, only_cache=False,
|
||||
manufacturer=None):
|
||||
|
@ -157,3 +162,44 @@ def get_attr_id_by_name(cluster, attr_name):
|
|||
"""Get the attribute id for a cluster attribute by its name."""
|
||||
return next((attrid for attrid, (attrname, datatype) in
|
||||
cluster.attributes.items() if attr_name == attrname), None)
|
||||
|
||||
|
||||
async def get_matched_clusters(source_zha_device, target_zha_device):
|
||||
"""Get matched input/output cluster pairs for 2 devices."""
|
||||
source_clusters = source_zha_device.async_get_zha_clusters()
|
||||
target_clusters = target_zha_device.async_get_zha_clusters()
|
||||
clusters_to_bind = []
|
||||
|
||||
for endpoint_id in source_clusters:
|
||||
for cluster_id in source_clusters[endpoint_id][OUT]:
|
||||
if cluster_id not in BINDABLE_CLUSTERS:
|
||||
continue
|
||||
for t_endpoint_id in target_clusters:
|
||||
if cluster_id in target_clusters[t_endpoint_id][IN]:
|
||||
cluster_pair = ClusterPair(
|
||||
source_cluster=source_clusters[
|
||||
endpoint_id][OUT][cluster_id],
|
||||
target_cluster=target_clusters[
|
||||
t_endpoint_id][IN][cluster_id]
|
||||
)
|
||||
clusters_to_bind.append(cluster_pair)
|
||||
return clusters_to_bind
|
||||
|
||||
|
||||
@callback
|
||||
def async_is_bindable_target(source_zha_device, target_zha_device):
|
||||
"""Determine if target is bindable to source."""
|
||||
source_clusters = source_zha_device.async_get_zha_clusters()
|
||||
target_clusters = target_zha_device.async_get_zha_clusters()
|
||||
|
||||
bindables = set(BINDABLE_CLUSTERS)
|
||||
for endpoint_id in source_clusters:
|
||||
for t_endpoint_id in target_clusters:
|
||||
matches = set(
|
||||
source_clusters[endpoint_id][OUT].keys()
|
||||
).intersection(
|
||||
target_clusters[t_endpoint_id][IN].keys()
|
||||
)
|
||||
if any(bindable in bindables for bindable in matches):
|
||||
return True
|
||||
return False
|
||||
|
|
Loading…
Reference in New Issue