From d0827a9129252e53363ca276561eaa0bece3954f Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 2 Oct 2021 21:57:49 -0400 Subject: [PATCH] ZHA support for additional entities on ElectricalMeasurement ZCL cluster (#56909) * Add electrical measurement type state attribute. * Add active_power_max attribute * Skip unsupported attributes on entity update * Fix tests * Create sensor only if the main attribute is supported * Refactor ElectricalMeasurement sensor to use attr specific divisor and multiplier * Multiple entities for electrical measurement cluster * Update discovery tests * Sensor clean up * update tests * Pylint --- .../zha/core/channels/homeautomation.py | 92 +++++- .../components/zha/core/registries.py | 1 - homeassistant/components/zha/sensor.py | 91 ++++-- tests/components/zha/test_channels.py | 13 +- tests/components/zha/test_sensor.py | 287 ++++++++++++++++-- tests/components/zha/zha_devices_list.py | 132 ++++++++ 6 files changed, 546 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index fc00db4f2d4..e25cc3eb0da 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -1,12 +1,15 @@ """Home automation channels module for Zigbee Home Automation.""" from __future__ import annotations +import enum + from zigpy.zcl.clusters import homeautomation from .. import registries from ..const import ( CHANNEL_ELECTRICAL_MEASUREMENT, REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_OP, SIGNAL_ATTR_UPDATED, ) from .base import ZigbeeChannel @@ -46,11 +49,36 @@ class ElectricalMeasurementChannel(ZigbeeChannel): CHANNEL_NAME = CHANNEL_ELECTRICAL_MEASUREMENT - REPORT_CONFIG = ({"attr": "active_power", "config": REPORT_CONFIG_DEFAULT},) + class MeasurementType(enum.IntFlag): + """Measurement types.""" + + ACTIVE_MEASUREMENT = 1 + REACTIVE_MEASUREMENT = 2 + APPARENT_MEASUREMENT = 4 + PHASE_A_MEASUREMENT = 8 + PHASE_B_MEASUREMENT = 16 + PHASE_C_MEASUREMENT = 32 + DC_MEASUREMENT = 64 + HARMONICS_MEASUREMENT = 128 + POWER_QUALITY_MEASUREMENT = 256 + + REPORT_CONFIG = ( + {"attr": "active_power", "config": REPORT_CONFIG_OP}, + {"attr": "active_power_max", "config": REPORT_CONFIG_DEFAULT}, + {"attr": "rms_current", "config": REPORT_CONFIG_OP}, + {"attr": "rms_current_max", "config": REPORT_CONFIG_DEFAULT}, + {"attr": "rms_voltage", "config": REPORT_CONFIG_OP}, + {"attr": "rms_voltage_max", "config": REPORT_CONFIG_DEFAULT}, + ) ZCL_INIT_ATTRS = { + "ac_current_divisor": True, + "ac_current_multiplier": True, "ac_power_divisor": True, - "power_divisor": True, "ac_power_multiplier": True, + "ac_voltage_divisor": True, + "ac_voltage_multiplier": True, + "measurement_type": True, + "power_divisor": True, "power_multiplier": True, } @@ -59,29 +87,65 @@ class ElectricalMeasurementChannel(ZigbeeChannel): self.debug("async_update") # This is a polling channel. Don't allow cache. - result = await self.get_attribute_value("active_power", from_cache=False) - if result is not None: - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - 0x050B, - "active_power", - result, - ) + attrs = [ + a["attr"] + for a in self.REPORT_CONFIG + if a["attr"] not in self.cluster.unsupported_attributes + ] + result = await self.get_attributes(attrs, from_cache=False) + if result: + for attr, value in result.items(): + self.async_send_signal( + f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", + self.cluster.attridx.get(attr, attr), + attr, + value, + ) @property - def divisor(self) -> int | None: + def ac_current_divisor(self) -> int: + """Return ac current divisor.""" + return self.cluster.get("ac_current_divisor") or 1 + + @property + def ac_current_multiplier(self) -> int: + """Return ac current multiplier.""" + return self.cluster.get("ac_current_multiplier") or 1 + + @property + def ac_voltage_divisor(self) -> int: + """Return ac voltage divisor.""" + return self.cluster.get("ac_voltage_divisor") or 1 + + @property + def ac_voltage_multiplier(self) -> int: + """Return ac voltage multiplier.""" + return self.cluster.get("ac_voltage_multiplier") or 1 + + @property + def ac_power_divisor(self) -> int: """Return active power divisor.""" return self.cluster.get( - "ac_power_divisor", self.cluster.get("power_divisor", 1) + "ac_power_divisor", self.cluster.get("power_divisor") or 1 ) @property - def multiplier(self) -> int | None: + def ac_power_multiplier(self) -> int: """Return active power divisor.""" return self.cluster.get( - "ac_power_multiplier", self.cluster.get("power_multiplier", 1) + "ac_power_multiplier", self.cluster.get("power_multiplier") or 1 ) + @property + def measurement_type(self) -> str | None: + """Return Measurement type.""" + meas_type = self.cluster.get("measurement_type") + if meas_type is None: + return None + + meas_type = self.MeasurementType(meas_type) + return ", ".join(m.name for m in self.MeasurementType if m in meas_type) + @registries.ZIGBEE_CHANNEL_REGISTRY.register( homeautomation.MeterIdentification.cluster_id diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 203867db17d..2bf324e3007 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -73,7 +73,6 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { zcl.clusters.general.MultistateInput.cluster_id: SENSOR, zcl.clusters.general.OnOff.cluster_id: SWITCH, zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR, - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: SENSOR, zcl.clusters.hvac.Fan.cluster_id: FAN, zcl.clusters.measurement.CarbonDioxideConcentration.cluster_id: SENSOR, zcl.clusters.measurement.CarbonMonoxideConcentration.cluster_id: SENSOR, diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 342fbd58d89..b2cc414ad5f 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, + DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -24,6 +25,8 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_ENERGY, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, @@ -129,6 +132,24 @@ class Sensor(ZhaEntity, SensorEntity): super().__init__(unique_id, zha_device, channels, **kwargs) self._channel: ChannelType = channels[0] + @classmethod + def create_entity( + cls, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> ZhaEntity | None: + """Entity Factory. + + Return entity if it is a supported configuration, otherwise return None + """ + channel = channels[0] + if cls.SENSOR_ATTR in channel.cluster.unsupported_attributes: + return None + + return cls(unique_id, zha_device, channels, **kwargs) + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() @@ -220,7 +241,7 @@ class Battery(Sensor): return state_attrs -@STRICT_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) +@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) class ElectricalMeasurement(Sensor): """Active power measurement.""" @@ -228,16 +249,32 @@ class ElectricalMeasurement(Sensor): _device_class = DEVICE_CLASS_POWER _state_class = STATE_CLASS_MEASUREMENT _unit = POWER_WATT + _div_mul_prefix = "ac_power" @property def should_poll(self) -> bool: """Return True if HA needs to poll for state changes.""" return True + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return device state attrs for sensor.""" + attrs = {} + if self._channel.measurement_type is not None: + attrs["measurement_type"] = self._channel.measurement_type + + max_attr_name = f"{self.SENSOR_ATTR}_max" + if (max_v := self._channel.cluster.get(max_attr_name)) is not None: + attrs[max_attr_name] = str(self.formatter(max_v)) + + return attrs + def formatter(self, value: int) -> int | float: """Return 'normalized' value.""" - value = value * self._channel.multiplier / self._channel.divisor - if value < 100 and self._channel.divisor > 1: + multiplier = getattr(self._channel, f"{self._div_mul_prefix}_multiplier") + divisor = getattr(self._channel, f"{self._div_mul_prefix}_divisor") + value = float(value * multiplier) / divisor + if value < 100 and divisor > 1: return round(value, self._decimals) return round(value) @@ -248,6 +285,36 @@ class ElectricalMeasurement(Sensor): await super().async_update() +@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) +class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_current"): + """RMS current measurement.""" + + SENSOR_ATTR = "rms_current" + _device_class = DEVICE_CLASS_CURRENT + _unit = ELECTRIC_CURRENT_AMPERE + _div_mul_prefix = "ac_current" + + @property + def should_poll(self) -> bool: + """Poll indirectly by ElectricalMeasurementSensor.""" + return False + + +@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) +class ElectricalMeasurementRMSVoltage(ElectricalMeasurement, id_suffix="rms_voltage"): + """RMS Voltage measurement.""" + + SENSOR_ATTR = "rms_voltage" + _device_class = DEVICE_CLASS_CURRENT + _unit = ELECTRIC_POTENTIAL_VOLT + _div_mul_prefix = "ac_voltage" + + @property + def should_poll(self) -> bool: + """Poll indirectly by ElectricalMeasurementSensor.""" + return False + + @STRICT_MATCH(generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER) @STRICT_MATCH(channel_names=CHANNEL_HUMIDITY) class Humidity(Sensor): @@ -298,24 +365,6 @@ class SmartEnergyMetering(Sensor): 0x0C: f"MJ/{TIME_SECONDS}", } - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], - **kwargs, - ) -> ZhaEntity | None: - """Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - se_channel = channels[0] - if cls.SENSOR_ATTR in se_channel.cluster.unsupported_attributes: - return None - - return cls(unique_id, zha_device, channels, **kwargs) - def formatter(self, value: int) -> int | float: """Pass through channel formatter.""" return self._channel.demand_formatter(value) diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index c1e60db31dd..e2543181a1a 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -152,7 +152,18 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): (0x0405, 1, {"measured_value"}), (0x0406, 1, {"occupancy"}), (0x0702, 1, {"instantaneous_demand"}), - (0x0B04, 1, {"active_power"}), + ( + 0x0B04, + 1, + { + "active_power", + "active_power_max", + "rms_current", + "rms_current_max", + "rms_voltage", + "rms_voltage_max", + }, + ), ], ) async def test_in_channel_config( diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 21731da72e6..918876fe448 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,5 +1,5 @@ """Test zha sensor.""" -from unittest import mock +import math import pytest import zigpy.profiles.zha @@ -9,6 +9,7 @@ import zigpy.zcl.clusters.measurement as measurement import zigpy.zcl.clusters.smartenergy as smartenergy from homeassistant.components.sensor import DOMAIN +from homeassistant.components.zha.core.const import ZHA_CHANNEL_READS_PER_REQ import homeassistant.config as config_util from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -17,6 +18,8 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, DEVICE_CLASS_ENERGY, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, @@ -30,6 +33,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.helpers import restore_state +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import dt as dt_util from .common import ( @@ -40,11 +44,52 @@ from .common import ( send_attribute_report, send_attributes_report, ) -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_e769900a_{}" +@pytest.fixture +async def elec_measurement_zigpy_dev(hass, zigpy_device_mock): + """Electric Measurement zigpy device.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + homeautomation.ElectricalMeasurement.cluster_id, + ], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SIMPLE_SENSOR, + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + } + }, + ) + zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 + zigpy_device.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS = { + "ac_current_divisor": 10, + "ac_current_multiplier": 1, + "ac_power_divisor": 10, + "ac_power_multiplier": 1, + "ac_voltage_divisor": 10, + "ac_voltage_multiplier": 1, + "measurement_type": 8, + "power_divisor": 10, + "power_multiplier": 1, + } + return zigpy_device + + +@pytest.fixture +async def elec_measurement_zha_dev(elec_measurement_zigpy_dev, zha_device_joined): + """Electric Measurement ZHA device.""" + + zha_dev = await zha_device_joined(elec_measurement_zigpy_dev) + zha_dev.available = True + return zha_dev + + async def async_test_humidity(hass, cluster, entity_id): """Test humidity sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 100}) @@ -109,26 +154,60 @@ async def async_test_smart_energy_summation(hass, cluster, entity_id): async def async_test_electrical_measurement(hass, cluster, entity_id): """Test electrical measurement sensor.""" - with mock.patch( - ( - "homeassistant.components.zha.core.channels.homeautomation" - ".ElectricalMeasurementChannel.divisor" - ), - new_callable=mock.PropertyMock, - ) as divisor_mock: - divisor_mock.return_value = 1 - await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000}) - assert_state(hass, entity_id, "100", POWER_WATT) + # update divisor cached value + await send_attributes_report(hass, cluster, {"ac_power_divisor": 1}) + await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000}) + assert_state(hass, entity_id, "100", POWER_WATT) - await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 1000}) - assert_state(hass, entity_id, "99", POWER_WATT) + await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 1000}) + assert_state(hass, entity_id, "99", POWER_WATT) - divisor_mock.return_value = 10 - await send_attributes_report(hass, cluster, {0: 1, 1291: 1000, 10: 5000}) - assert_state(hass, entity_id, "100", POWER_WATT) + await send_attributes_report(hass, cluster, {"ac_power_divisor": 10}) + await send_attributes_report(hass, cluster, {0: 1, 1291: 1000, 10: 5000}) + assert_state(hass, entity_id, "100", POWER_WATT) - await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 5000}) - assert_state(hass, entity_id, "9.9", POWER_WATT) + await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 5000}) + assert_state(hass, entity_id, "9.9", POWER_WATT) + + assert "active_power_max" not in hass.states.get(entity_id).attributes + await send_attributes_report(hass, cluster, {0: 1, 0x050D: 88, 10: 5000}) + assert hass.states.get(entity_id).attributes["active_power_max"] == "8.8" + + +async def async_test_em_rms_current(hass, cluster, entity_id): + """Test electrical measurement RMS Current sensor.""" + + await send_attributes_report(hass, cluster, {0: 1, 0x0508: 1234, 10: 1000}) + assert_state(hass, entity_id, "1.2", ELECTRIC_CURRENT_AMPERE) + + await send_attributes_report(hass, cluster, {"ac_current_divisor": 10}) + await send_attributes_report(hass, cluster, {0: 1, 0x0508: 236, 10: 1000}) + assert_state(hass, entity_id, "23.6", ELECTRIC_CURRENT_AMPERE) + + await send_attributes_report(hass, cluster, {0: 1, 0x0508: 1236, 10: 1000}) + assert_state(hass, entity_id, "124", ELECTRIC_CURRENT_AMPERE) + + assert "rms_current_max" not in hass.states.get(entity_id).attributes + await send_attributes_report(hass, cluster, {0: 1, 0x050A: 88, 10: 5000}) + assert hass.states.get(entity_id).attributes["rms_current_max"] == "8.8" + + +async def async_test_em_rms_voltage(hass, cluster, entity_id): + """Test electrical measurement RMS Voltage sensor.""" + + await send_attributes_report(hass, cluster, {0: 1, 0x0505: 1234, 10: 1000}) + assert_state(hass, entity_id, "123", ELECTRIC_POTENTIAL_VOLT) + + await send_attributes_report(hass, cluster, {0: 1, 0x0505: 234, 10: 1000}) + assert_state(hass, entity_id, "23.4", ELECTRIC_POTENTIAL_VOLT) + + await send_attributes_report(hass, cluster, {"ac_voltage_divisor": 100}) + await send_attributes_report(hass, cluster, {0: 1, 0x0505: 2236, 10: 1000}) + assert_state(hass, entity_id, "22.4", ELECTRIC_POTENTIAL_VOLT) + + assert "rms_voltage_max" not in hass.states.get(entity_id).attributes + await send_attributes_report(hass, cluster, {0: 1, 0x0507: 888, 10: 5000}) + assert hass.states.get(entity_id).attributes["rms_voltage_max"] == "8.9" async def async_test_powerconfiguration(hass, cluster, entity_id): @@ -211,9 +290,25 @@ async def async_test_powerconfiguration(hass, cluster, entity_id): homeautomation.ElectricalMeasurement.cluster_id, "electrical_measurement", async_test_electrical_measurement, - 1, - None, - None, + 6, + {"ac_power_divisor": 1000, "ac_power_multiplier": 1}, + {"rms_current", "rms_voltage"}, + ), + ( + homeautomation.ElectricalMeasurement.cluster_id, + "electrical_measurement_rms_current", + async_test_em_rms_current, + 6, + {"ac_current_divisor": 1000, "ac_current_multiplier": 1}, + {"active_power", "rms_voltage"}, + ), + ( + homeautomation.ElectricalMeasurement.cluster_id, + "electrical_measurement_rms_voltage", + async_test_em_rms_voltage, + 6, + {"ac_voltage_divisor": 10, "ac_voltage_multiplier": 1}, + {"active_power", "rms_current"}, ), ( general.PowerConfiguration.cluster_id, @@ -255,7 +350,10 @@ async def test_sensor( if unsupported_attrs: for attr in unsupported_attrs: cluster.add_unsupported_attribute(attr) - if cluster_id == smartenergy.Metering.cluster_id: + if cluster_id in ( + smartenergy.Metering.cluster_id, + homeautomation.ElectricalMeasurement.cluster_id, + ): # this one is mains powered zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 cluster.PLUGGED_ATTR_READS = read_plug @@ -432,35 +530,60 @@ async def test_electrical_measurement_init( assert int(hass.states.get(entity_id).state) == 100 channel = zha_device.channels.pools[0].all_channels["1:0x0b04"] - assert channel.divisor == 1 - assert channel.multiplier == 1 + assert channel.ac_power_divisor == 1 + assert channel.ac_power_multiplier == 1 # update power divisor await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0403: 5, 10: 1000}) - assert channel.divisor == 5 - assert channel.multiplier == 1 + assert channel.ac_power_divisor == 5 + assert channel.ac_power_multiplier == 1 assert hass.states.get(entity_id).state == "4.0" await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0605: 10, 10: 1000}) - assert channel.divisor == 10 - assert channel.multiplier == 1 + assert channel.ac_power_divisor == 10 + assert channel.ac_power_multiplier == 1 assert hass.states.get(entity_id).state == "3.0" # update power multiplier await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0402: 6, 10: 1000}) - assert channel.divisor == 10 - assert channel.multiplier == 6 + assert channel.ac_power_divisor == 10 + assert channel.ac_power_multiplier == 6 assert hass.states.get(entity_id).state == "12.0" await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0604: 20, 10: 1000}) - assert channel.divisor == 10 - assert channel.multiplier == 20 + assert channel.ac_power_divisor == 10 + assert channel.ac_power_multiplier == 20 assert hass.states.get(entity_id).state == "60.0" @pytest.mark.parametrize( "cluster_id, unsupported_attributes, entity_ids, missing_entity_ids", ( + ( + homeautomation.ElectricalMeasurement.cluster_id, + {"rms_voltage", "rms_current"}, + {"electrical_measurement"}, + { + "electrical_measurement_rms_voltage", + "electrical_measurement_rms_current", + }, + ), + ( + homeautomation.ElectricalMeasurement.cluster_id, + {"rms_current"}, + {"electrical_measurement_rms_voltage", "electrical_measurement"}, + {"electrical_measurement_rms_current"}, + ), + ( + homeautomation.ElectricalMeasurement.cluster_id, + set(), + { + "electrical_measurement_rms_voltage", + "electrical_measurement", + "electrical_measurement_rms_current", + }, + set(), + ), ( smartenergy.Metering.cluster_id, { @@ -650,3 +773,101 @@ async def test_se_summation_uom( await zha_device_joined(zigpy_device) assert_state(hass, entity_id, expected_state, expected_uom) + + +@pytest.mark.parametrize( + "raw_measurement_type, expected_type", + ( + (1, "ACTIVE_MEASUREMENT"), + (8, "PHASE_A_MEASUREMENT"), + (9, "ACTIVE_MEASUREMENT, PHASE_A_MEASUREMENT"), + ( + 15, + "ACTIVE_MEASUREMENT, REACTIVE_MEASUREMENT, APPARENT_MEASUREMENT, PHASE_A_MEASUREMENT", + ), + ), +) +async def test_elec_measurement_sensor_type( + hass, + elec_measurement_zigpy_dev, + raw_measurement_type, + expected_type, + zha_device_joined, +): + """Test zha electrical measurement sensor type.""" + + entity_id = ENTITY_ID_PREFIX.format("electrical_measurement") + zigpy_dev = elec_measurement_zigpy_dev + zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[ + "measurement_type" + ] = raw_measurement_type + + await zha_device_joined(zigpy_dev) + + state = hass.states.get(entity_id) + assert state is not None + assert state.attributes["measurement_type"] == expected_type + + +@pytest.mark.parametrize( + "supported_attributes", + ( + set(), + { + "active_power", + "active_power_max", + "rms_current", + "rms_current_max", + "rms_voltage", + "rms_voltage_max", + }, + { + "active_power", + }, + { + "active_power", + "active_power_max", + }, + { + "rms_current", + "rms_current_max", + }, + { + "rms_voltage", + "rms_voltage_max", + }, + ), +) +async def test_elec_measurement_skip_unsupported_attribute( + hass, + elec_measurement_zha_dev, + supported_attributes, +): + """Test zha electrical measurement skipping update of unsupported attributes.""" + + entity_id = ENTITY_ID_PREFIX.format("electrical_measurement") + zha_dev = elec_measurement_zha_dev + + cluster = zha_dev.device.endpoints[1].electrical_measurement + + all_attrs = { + "active_power", + "active_power_max", + "rms_current", + "rms_current_max", + "rms_voltage", + "rms_voltage_max", + } + for attr in all_attrs - supported_attributes: + cluster.add_unsupported_attribute(attr) + cluster.read_attributes.reset_mock() + + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + assert cluster.read_attributes.call_count == math.ceil( + len(supported_attributes) / ZHA_CHANNEL_READS_PER_REQ + ) + read_attrs = { + a for call in cluster.read_attributes.call_args_list for a in call[0][0] + } + assert read_attrs == supported_attributes diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 531e9649ec3..6276aa12068 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -117,6 +117,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.centralite_3210_l_77665544_electrical_measurement", + "sensor.centralite_3210_l_77665544_electrical_measurement_rms_current", + "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", "sensor.centralite_3210_l_77665544_smartenergy_metering", "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", "switch.centralite_3210_l_77665544_on_off", @@ -142,6 +144,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "CentraLite", @@ -1436,6 +1448,8 @@ DEVICES = [ "sensor.lumi_lumi_plug_maus01_77665544_analog_input", "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", + "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_current", + "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", "switch.lumi_lumi_plug_maus01_77665544_on_off", ], DEV_SIG_ENT_MAP: { @@ -1459,6 +1473,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-100-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", @@ -1493,6 +1517,8 @@ DEVICES = [ "light.lumi_lumi_relay_c2acn01_77665544_on_off", "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", + "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_current", + "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -1505,6 +1531,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", + }, ("light", "00:11:22:33:44:55:66:77-2"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", @@ -2566,6 +2602,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", + "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_current", + "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { @@ -2578,6 +2616,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], SIG_MANUFACTURER: "OSRAM", @@ -2598,6 +2646,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.osram_plug_01_77665544_electrical_measurement", + "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", + "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", "switch.osram_plug_01_77665544_on_off", ], DEV_SIG_ENT_MAP: { @@ -2611,6 +2661,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], SIG_MANUFACTURER: "OSRAM", @@ -2870,6 +2930,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", + "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", + "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", "switch.securifi_ltd_unk_model_77665544_on_off", ], DEV_SIG_ENT_MAP: { @@ -2883,6 +2945,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0019"], SIG_MANUFACTURER: "Securifi Ltd.", @@ -2948,6 +3020,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "light.sercomm_corp_sz_esw01_77665544_on_off", "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", + "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_current", + "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_voltage", "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", ], @@ -2972,6 +3046,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], SIG_MANUFACTURER: "Sercomm Corp.", @@ -3035,6 +3119,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", + "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", + "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", "switch.sinope_technologies_rm3250zb_77665544_on_off", ], DEV_SIG_ENT_MAP: { @@ -3048,6 +3134,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "Sinope Technologies", @@ -3075,6 +3171,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "climate.sinope_technologies_th1123zb_77665544_thermostat", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", + "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", + "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", "sensor.sinope_technologies_th1123zb_77665544_temperature", ], DEV_SIG_ENT_MAP: { @@ -3093,6 +3191,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "Sinope Technologies", @@ -3120,6 +3228,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", + "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", + "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", "sensor.sinope_technologies_th1124zb_77665544_temperature", "climate.sinope_technologies_th1124zb_77665544_thermostat", ], @@ -3139,6 +3249,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "Sinope Technologies", @@ -3159,6 +3279,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.smartthings_outletv4_77665544_electrical_measurement", + "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_current", + "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", "switch.smartthings_outletv4_77665544_on_off", ], DEV_SIG_ENT_MAP: { @@ -3172,6 +3294,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput",