From 2e2e336921a0d3ae6d1cfc384997e53ce027c5d7 Mon Sep 17 00:00:00 2001 From: abmantis Date: Mon, 9 Dec 2024 22:37:24 +0000 Subject: [PATCH 1/5] Add support for rms_current_ph_b/c --- zha/application/platforms/sensor/__init__.py | 50 ++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/zha/application/platforms/sensor/__init__.py b/zha/application/platforms/sensor/__init__.py index f5341d98c..968078992 100644 --- a/zha/application/platforms/sensor/__init__.py +++ b/zha/application/platforms/sensor/__init__.py @@ -686,6 +686,56 @@ class ElectricalMeasurementRMSCurrent(PolledElectricalMeasurement): _div_mul_prefix = "ac_current" +@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) +class ElectricalMeasurementRMSCurrentPhB(ElectricalMeasurementRMSCurrent): + """RMS current measurement.""" + + _attribute_name = "rms_current_ph_b" + _unique_id_suffix = "rms_current_ph_b" + _attr_translation_key: str = "rms_current_ph_b" + + @classmethod + def create_platform_entity( + cls: type[Self], + unique_id: str, + cluster_handlers: list[ClusterHandler], + endpoint: Endpoint, + device: Device, + **kwargs: Any, + ) -> Self | None: + """Entity Factory.""" + if cluster_handlers[0].cluster.get(cls._attribute_name) is None: + return None + return super().create_platform_entity( + unique_id, cluster_handlers, endpoint, device, **kwargs + ) + + +@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) +class ElectricalMeasurementRMSCurrentPhC(ElectricalMeasurementRMSCurrent): + """RMS current measurement.""" + + _attribute_name: str = "rms_current_ph_c" + _unique_id_suffix: str = "rms_current_ph_c" + _attr_translation_key: str = "rms_current_ph_c" + + @classmethod + def create_platform_entity( + cls: type[Self], + unique_id: str, + cluster_handlers: list[ClusterHandler], + endpoint: Endpoint, + device: Device, + **kwargs: Any, + ) -> Self | None: + """Entity Factory.""" + if cluster_handlers[0].cluster.get(cls._attribute_name) is None: + return None + return super().create_platform_entity( + unique_id, cluster_handlers, endpoint, device, **kwargs + ) + + @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) class ElectricalMeasurementRMSVoltage(PolledElectricalMeasurement): """RMS Voltage measurement.""" From abfd57416d51de68b7b3dbf30b7b5f8b25f93af5 Mon Sep 17 00:00:00 2001 From: abmantis Date: Tue, 10 Dec 2024 18:59:41 +0000 Subject: [PATCH 2/5] Make max attribute name overrideable; Add tests --- tests/test_sensor.py | 46 ++++++++++++++++--- zha/application/platforms/sensor/__init__.py | 19 +++++++- zha/zigbee/cluster_handlers/homeautomation.py | 16 +++++++ 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 25bbb4a36..38c6c8950 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import Awaitable, Callable from datetime import UTC, datetime +from functools import partial import math from typing import Any, Optional from unittest.mock import AsyncMock, MagicMock @@ -298,22 +299,27 @@ async def async_test_em_power_factor( async def async_test_em_rms_current( - zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity + current_attrid: int, + current_max_attrid: int, + current_max_attr_name: str, + zha_gateway: Gateway, + cluster: Cluster, + entity: PlatformEntity, ) -> None: """Test electrical measurement RMS Current sensor.""" - await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0508: 1234}) + await send_attributes_report(zha_gateway, cluster, {0: 1, current_attrid: 1234}) assert_state(entity, 1.2, "A") await send_attributes_report(zha_gateway, cluster, {"ac_current_divisor": 10}) - await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0508: 236}) + await send_attributes_report(zha_gateway, cluster, {0: 1, current_attrid: 236}) assert_state(entity, 23.6, "A") - await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0508: 1236}) + await send_attributes_report(zha_gateway, cluster, {0: 1, current_attrid: 1236}) assert_state(entity, 124, "A") - await send_attributes_report(zha_gateway, cluster, {0: 1, 0x050A: 88}) - assert entity.state["rms_current_max"] == 8.8 + await send_attributes_report(zha_gateway, cluster, {0: 1, current_max_attrid: 88}) + assert entity.state[current_max_attr_name] == 8.8 async def async_test_em_rms_voltage( @@ -515,10 +521,32 @@ async def async_test_change_source_timestamp( ( homeautomation.ElectricalMeasurement.cluster_id, sensor.ElectricalMeasurementRMSCurrent, - async_test_em_rms_current, + partial(async_test_em_rms_current, 0x0508, 0x050A, "rms_current_max"), {"ac_current_divisor": 1000, "ac_current_multiplier": 1}, {"active_power", "apparent_power", "rms_voltage"}, ), + ( + homeautomation.ElectricalMeasurement.cluster_id, + sensor.ElectricalMeasurementRMSCurrentPhB, + partial(async_test_em_rms_current, 0x0908, 0x090A, "rms_current_max_ph_b"), + { + "ac_current_divisor": 1000, + "ac_current_multiplier": 1, + "rms_current_ph_b": 0, + }, + {"active_power", "apparent_power", "rms_voltage"}, + ), + ( + homeautomation.ElectricalMeasurement.cluster_id, + sensor.ElectricalMeasurementRMSCurrentPhC, + partial(async_test_em_rms_current, 0x0A08, 0x0A0A, "rms_current_max_ph_c"), + { + "ac_current_divisor": 1000, + "ac_current_multiplier": 1, + "rms_current_ph_c": 0, + }, + {"active_power", "apparent_power", "rms_voltage"}, + ), ( homeautomation.ElectricalMeasurement.cluster_id, sensor.ElectricalMeasurementRMSVoltage, @@ -1122,7 +1150,11 @@ async def test_elec_measurement_skip_unsupported_attribute( "active_power_max", "apparent_power", "rms_current", + "rms_current_ph_b", + "rms_current_ph_c", "rms_current_max", + "rms_current_max_ph_b", + "rms_current_max_ph_c", "rms_voltage", "rms_voltage_max", "power_factor", diff --git a/zha/application/platforms/sensor/__init__.py b/zha/application/platforms/sensor/__init__.py index 968078992..398552284 100644 --- a/zha/application/platforms/sensor/__init__.py +++ b/zha/application/platforms/sensor/__init__.py @@ -617,9 +617,14 @@ def __init__( super().__init__(unique_id, cluster_handlers, endpoint, device, **kwargs) self._attr_extra_state_attribute_names: set[str] = { "measurement_type", - f"{self._attribute_name}_max", + self._max_attribute_name, } + @property + def _max_attribute_name(self) -> str: + """Return the max attribute name.""" + return f"{self._attribute_name}_max" + @property def state(self) -> dict[str, Any]: """Return the state for this sensor.""" @@ -627,7 +632,7 @@ def state(self) -> dict[str, Any]: if self._cluster_handler.measurement_type is not None: response["measurement_type"] = self._cluster_handler.measurement_type - max_attr_name = f"{self._attribute_name}_max" + max_attr_name = self._max_attribute_name if not hasattr(self._cluster_handler.cluster.AttributeDefs, max_attr_name): return response @@ -694,6 +699,11 @@ class ElectricalMeasurementRMSCurrentPhB(ElectricalMeasurementRMSCurrent): _unique_id_suffix = "rms_current_ph_b" _attr_translation_key: str = "rms_current_ph_b" + @property + def _max_attribute_name(self) -> str: + """Return the max attribute name.""" + return "rms_current_max_ph_b" + @classmethod def create_platform_entity( cls: type[Self], @@ -719,6 +729,11 @@ class ElectricalMeasurementRMSCurrentPhC(ElectricalMeasurementRMSCurrent): _unique_id_suffix: str = "rms_current_ph_c" _attr_translation_key: str = "rms_current_ph_c" + @property + def _max_attribute_name(self) -> str: + """Return the max attribute name.""" + return "rms_current_max_ph_c" + @classmethod def create_platform_entity( cls: type[Self], diff --git a/zha/zigbee/cluster_handlers/homeautomation.py b/zha/zigbee/cluster_handlers/homeautomation.py index 57067c9df..180ab286e 100644 --- a/zha/zigbee/cluster_handlers/homeautomation.py +++ b/zha/zigbee/cluster_handlers/homeautomation.py @@ -77,10 +77,26 @@ class MeasurementType(enum.IntFlag): attr=ElectricalMeasurement.AttributeDefs.rms_current.name, config=REPORT_CONFIG_OP, ), + AttrReportConfig( + attr=ElectricalMeasurement.AttributeDefs.rms_current_ph_b.name, + config=REPORT_CONFIG_OP, + ), + AttrReportConfig( + attr=ElectricalMeasurement.AttributeDefs.rms_current_ph_c.name, + config=REPORT_CONFIG_OP, + ), AttrReportConfig( attr=ElectricalMeasurement.AttributeDefs.rms_current_max.name, config=REPORT_CONFIG_DEFAULT, ), + AttrReportConfig( + attr=ElectricalMeasurement.AttributeDefs.rms_current_max_ph_b.name, + config=REPORT_CONFIG_DEFAULT, + ), + AttrReportConfig( + attr=ElectricalMeasurement.AttributeDefs.rms_current_max_ph_c.name, + config=REPORT_CONFIG_DEFAULT, + ), AttrReportConfig( attr=ElectricalMeasurement.AttributeDefs.rms_voltage.name, config=REPORT_CONFIG_OP, From a924e3bbcd8494ef379019888d19c1c079ac987a Mon Sep 17 00:00:00 2001 From: abmantis Date: Tue, 10 Dec 2024 20:22:42 +0000 Subject: [PATCH 3/5] Use attribute for conditional create_platform_entity --- zha/application/platforms/sensor/__init__.py | 60 ++++---------------- 1 file changed, 11 insertions(+), 49 deletions(-) diff --git a/zha/application/platforms/sensor/__init__.py b/zha/application/platforms/sensor/__init__.py index 398552284..809ee658b 100644 --- a/zha/application/platforms/sensor/__init__.py +++ b/zha/application/platforms/sensor/__init__.py @@ -157,6 +157,7 @@ class Sensor(PlatformEntity): _attr_native_unit_of_measurement: str | None = None _attr_device_class: SensorDeviceClass | None = None _attr_state_class: SensorStateClass | None = None + _skip_creation_if_none: bool = False @classmethod def create_platform_entity( @@ -183,6 +184,12 @@ def create_platform_entity( ) return None + if ( + cls._skip_creation_if_none + and cluster_handlers[0].cluster.get(cls._attribute_name) is None + ): + return None + return cls(unique_id, cluster_handlers, endpoint, device, **kwargs) def __init__( @@ -698,28 +705,13 @@ class ElectricalMeasurementRMSCurrentPhB(ElectricalMeasurementRMSCurrent): _attribute_name = "rms_current_ph_b" _unique_id_suffix = "rms_current_ph_b" _attr_translation_key: str = "rms_current_ph_b" + _skip_creation_if_none = True @property def _max_attribute_name(self) -> str: """Return the max attribute name.""" return "rms_current_max_ph_b" - @classmethod - def create_platform_entity( - cls: type[Self], - unique_id: str, - cluster_handlers: list[ClusterHandler], - endpoint: Endpoint, - device: Device, - **kwargs: Any, - ) -> Self | None: - """Entity Factory.""" - if cluster_handlers[0].cluster.get(cls._attribute_name) is None: - return None - return super().create_platform_entity( - unique_id, cluster_handlers, endpoint, device, **kwargs - ) - @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) class ElectricalMeasurementRMSCurrentPhC(ElectricalMeasurementRMSCurrent): @@ -728,28 +720,13 @@ class ElectricalMeasurementRMSCurrentPhC(ElectricalMeasurementRMSCurrent): _attribute_name: str = "rms_current_ph_c" _unique_id_suffix: str = "rms_current_ph_c" _attr_translation_key: str = "rms_current_ph_c" + _skip_creation_if_none = True @property def _max_attribute_name(self) -> str: """Return the max attribute name.""" return "rms_current_max_ph_c" - @classmethod - def create_platform_entity( - cls: type[Self], - unique_id: str, - cluster_handlers: list[ClusterHandler], - endpoint: Endpoint, - device: Device, - **kwargs: Any, - ) -> Self | None: - """Entity Factory.""" - if cluster_handlers[0].cluster.get(cls._attribute_name) is None: - return None - return super().create_platform_entity( - unique_id, cluster_handlers, endpoint, device, **kwargs - ) - @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) class ElectricalMeasurementRMSVoltage(PolledElectricalMeasurement): @@ -1162,29 +1139,14 @@ class SmartEnergySummationReceived(PolledSmartEnergySummation): _unique_id_suffix = "summation_received" _attr_translation_key: str = "summation_received" - @classmethod - def create_platform_entity( - cls: type[Self], - unique_id: str, - cluster_handlers: list[ClusterHandler], - endpoint: Endpoint, - device: Device, - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - This attribute only started to be initialized in HA 2024.2.0, + """ This attribute only started to be initialized in HA 2024.2.0, so the entity would be created on the first HA start after the upgrade for existing devices, as the initialization to see if an attribute is unsupported happens later in the background. To avoid creating unnecessary entities for existing devices, wait until the attribute was properly initialized once for now. """ - if cluster_handlers[0].cluster.get(cls._attribute_name) is None: - return None - return super().create_platform_entity( - unique_id, cluster_handlers, endpoint, device, **kwargs - ) + _skip_creation_if_none = True @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_PRESSURE) From 30bfcbc3b66bdc263070749180b42565c43cbc22 Mon Sep 17 00:00:00 2001 From: abmantis Date: Tue, 10 Dec 2024 20:29:36 +0000 Subject: [PATCH 4/5] Fix cluster handler config test --- tests/test_cluster_handlers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_cluster_handlers.py b/tests/test_cluster_handlers.py index 237a46b16..7827be9ad 100644 --- a/tests/test_cluster_handlers.py +++ b/tests/test_cluster_handlers.py @@ -285,11 +285,17 @@ async def poll_control_device_mock(zha_gateway: Gateway) -> Device: zigpy.zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id, 1, { + "ac_frequency", + "ac_frequency_max", "active_power", "active_power_max", "apparent_power", "rms_current", + "rms_current_ph_b", + "rms_current_ph_c", "rms_current_max", + "rms_current_max_b", + "rms_current_max_c", "rms_voltage", "rms_voltage_max", }, From a0296f564c4c0490d60ed1993c0cb3c6108cdb8b Mon Sep 17 00:00:00 2001 From: abmantis Date: Tue, 24 Dec 2024 16:03:03 +0000 Subject: [PATCH 5/5] Use ph B as base for C to reduce duplicated attrs --- zha/application/platforms/sensor/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zha/application/platforms/sensor/__init__.py b/zha/application/platforms/sensor/__init__.py index 809ee658b..00edd8b9d 100644 --- a/zha/application/platforms/sensor/__init__.py +++ b/zha/application/platforms/sensor/__init__.py @@ -714,13 +714,12 @@ def _max_attribute_name(self) -> str: @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) -class ElectricalMeasurementRMSCurrentPhC(ElectricalMeasurementRMSCurrent): +class ElectricalMeasurementRMSCurrentPhC(ElectricalMeasurementRMSCurrentPhB): """RMS current measurement.""" _attribute_name: str = "rms_current_ph_c" _unique_id_suffix: str = "rms_current_ph_c" _attr_translation_key: str = "rms_current_ph_c" - _skip_creation_if_none = True @property def _max_attribute_name(self) -> str: