diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index 6d4899be37c..48f69ffbf2d 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -49,8 +49,8 @@ _LOGGER = logging.getLogger(__name__) class AttrReportConfig(TypedDict, total=True): """Configuration to report for the attributes.""" - # Could be either an attribute name or attribute id - attr: str | int + # An attribute name + attr: str # The config for the attribute reporting configuration consists of a tuple for # (minimum_reported_time_interval_s, maximum_reported_time_interval_s, value_delta) config: tuple[int, int, int | float] @@ -130,15 +130,13 @@ class ZigbeeChannel(LogMixin): unique_id = ch_pool.unique_id.replace("-", ":") self._unique_id = f"{unique_id}:0x{cluster.cluster_id:04x}" if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG: - attr = self.REPORT_CONFIG[0].get("attr") - if isinstance(attr, str): - attribute: ZCLAttributeDef = self.cluster.attributes_by_name.get(attr) - if attribute is not None: - self.value_attribute = attribute.id - else: - self.value_attribute = None + attr_def: ZCLAttributeDef | None = self.cluster.attributes_by_name.get( + self.REPORT_CONFIG[0]["attr"] + ) + if attr_def is not None: + self.value_attribute = attr_def.id else: - self.value_attribute = attr + self.value_attribute = None self._status = ChannelStatus.CREATED self._cluster.add_listener(self) self.data_cache: dict[str, Enum] = {} @@ -233,7 +231,12 @@ class ZigbeeChannel(LogMixin): for attr_report in self.REPORT_CONFIG: attr, config = attr_report["attr"], attr_report["config"] - attr_name = self.cluster.attributes.get(attr, [attr])[0] + + try: + attr_name = self.cluster.find_attribute(attr).name + except KeyError: + attr_name = attr + event_data[attr_name] = { "min": config[0], "max": config[1], @@ -282,7 +285,7 @@ class ZigbeeChannel(LogMixin): ) def _configure_reporting_status( - self, attrs: dict[int | str, tuple[int, int, float | int]], res: list | tuple + self, attrs: dict[str, tuple[int, int, float | int]], res: list | tuple ) -> None: """Parse configure reporting result.""" if isinstance(res, (Exception, ConfigureReportingResponseRecord)): @@ -304,14 +307,14 @@ class ZigbeeChannel(LogMixin): return failed = [ - self.cluster.attributes.get(r.attrid, [r.attrid])[0] - for r in res - if r.status != Status.SUCCESS + self.cluster.find_attribute(record.attrid).name + for record in res + if record.status != Status.SUCCESS ] - attributes = {self.cluster.attributes.get(r, [r])[0] for r in attrs} + self.debug( "Successfully configured reporting for '%s' on '%s' cluster", - attributes - set(failed), + set(attrs) - set(failed), self.name, ) self.debug( diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 9c43a76ea85..b8542433e7c 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -5,9 +5,12 @@ from unittest import mock from unittest.mock import AsyncMock, patch import pytest +import zigpy.endpoint import zigpy.profiles.zha import zigpy.types as t +from zigpy.zcl import foundation import zigpy.zcl.clusters +import zigpy.zdo.types as zdo_t import homeassistant.components.zha.core.channels as zha_channels import homeassistant.components.zha.core.channels.base as base_channels @@ -726,3 +729,56 @@ async def test_cluster_no_ep_attribute(m1, zha_device_mock) -> None: pools = {pool.id: pool for pool in channels.pools} assert "1:0x042e" in pools[1].all_channels assert pools[1].all_channels["1:0x042e"].name + + +async def test_configure_reporting(hass: HomeAssistant) -> None: + """Test setting up a channel and configuring attribute reporting in two batches.""" + + class TestZigbeeChannel(base_channels.ZigbeeChannel): + BIND = True + REPORT_CONFIG = ( + # By name + base_channels.AttrReportConfig(attr="current_x", config=(1, 60, 1)), + base_channels.AttrReportConfig(attr="current_hue", config=(1, 60, 2)), + base_channels.AttrReportConfig(attr="color_temperature", config=(1, 60, 3)), + base_channels.AttrReportConfig(attr="current_y", config=(1, 60, 4)), + ) + + mock_ep = mock.AsyncMock(spec_set=zigpy.endpoint.Endpoint) + mock_ep.device.zdo = AsyncMock() + + cluster = zigpy.zcl.clusters.lighting.Color(mock_ep) + cluster.bind = AsyncMock( + spec_set=cluster.bind, + return_value=[zdo_t.Status.SUCCESS], # ZDOCmd.Bind_rsp + ) + cluster.configure_reporting_multiple = AsyncMock( + spec_set=cluster.configure_reporting_multiple, + return_value=[ + foundation.ConfigureReportingResponseRecord( + status=foundation.Status.SUCCESS + ) + ], + ) + + ch_pool = mock.AsyncMock(spec_set=zha_channels.ChannelPool) + ch_pool.skip_configuration = False + + channel = TestZigbeeChannel(cluster, ch_pool) + await channel.async_configure() + + # Since we request reporting for five attributes, we need to make two calls (3 + 1) + assert cluster.configure_reporting_multiple.mock_calls == [ + mock.call( + { + "current_x": (1, 60, 1), + "current_hue": (1, 60, 2), + "color_temperature": (1, 60, 3), + } + ), + mock.call( + { + "current_y": (1, 60, 4), + } + ), + ]