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", }, diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 663711c34..65c2a5c19 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 832ebfe3d..11e9c8769 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_no_attr_cache: bool = False @classmethod def create_platform_entity( @@ -183,6 +184,12 @@ def create_platform_entity( ) return None + if ( + cls._skip_creation_if_no_attr_cache + and cluster_handlers[0].cluster.get(cls._attribute_name) is None + ): + return None + return cls(unique_id, cluster_handlers, endpoint, device, **kwargs) def __init__( @@ -622,6 +629,7 @@ class ElectricalMeasurement(PollableSensor): _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement: str = UnitOfPower.WATT + _attr_max_attribute_name: str = None _div_mul_prefix: str | None = "ac_power" def __init__( @@ -636,9 +644,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 self._attr_max_attribute_name or f"{self._attribute_name}_max" + @property def state(self) -> dict[str, Any]: """Return the state for this sensor.""" @@ -646,7 +659,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 @@ -705,6 +718,28 @@ 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" + _skip_creation_if_no_attr_cache = True + _attr_max_attribute_name: str = "rms_current_max_ph_b" + + +@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" + _skip_creation_if_no_attr_cache = True + _attr_max_attribute_name: str = "rms_current_max_ph_c" + + @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) class ElectricalMeasurementRMSVoltage(PolledElectricalMeasurement): """RMS Voltage measurement.""" @@ -1115,30 +1150,15 @@ class SmartEnergySummationReceived(PolledSmartEnergySummation): _attribute_name = "current_summ_received" _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, - 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 - ) + """ + 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. + """ + _skip_creation_if_no_attr_cache = True @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_PRESSURE) 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,