Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set up reporting on AC attribute divisor and multiplier #348

Merged
merged 12 commits into from
Jan 27, 2025
49 changes: 29 additions & 20 deletions tests/test_cluster_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ async def poll_control_device_mock(zha_gateway: Gateway) -> Device:
@pytest.mark.parametrize(
("cluster_id", "bind_count", "attrs"),
[
(zigpy.zcl.clusters.general.Basic.cluster_id, 0, {}),
(zigpy.zcl.clusters.general.Basic.cluster_id, 0, set()),
(
zigpy.zcl.clusters.general.PowerConfiguration.cluster_id,
1,
Expand All @@ -183,13 +183,13 @@ async def poll_control_device_mock(zha_gateway: Gateway) -> Device:
1,
{"current_temperature"},
),
(zigpy.zcl.clusters.general.Identify.cluster_id, 0, {}),
(zigpy.zcl.clusters.general.Groups.cluster_id, 0, {}),
(zigpy.zcl.clusters.general.Scenes.cluster_id, 1, {}),
(zigpy.zcl.clusters.general.Identify.cluster_id, 0, set()),
(zigpy.zcl.clusters.general.Groups.cluster_id, 0, set()),
(zigpy.zcl.clusters.general.Scenes.cluster_id, 1, set()),
(zigpy.zcl.clusters.general.OnOff.cluster_id, 1, {"on_off"}),
(zigpy.zcl.clusters.general.OnOffConfiguration.cluster_id, 1, {}),
(zigpy.zcl.clusters.general.OnOffConfiguration.cluster_id, 1, set()),
(zigpy.zcl.clusters.general.LevelControl.cluster_id, 1, {"current_level"}),
(zigpy.zcl.clusters.general.Alarms.cluster_id, 1, {}),
(zigpy.zcl.clusters.general.Alarms.cluster_id, 1, set()),
(zigpy.zcl.clusters.general.AnalogInput.cluster_id, 1, {"present_value"}),
(zigpy.zcl.clusters.general.AnalogOutput.cluster_id, 1, {"present_value"}),
(zigpy.zcl.clusters.general.AnalogValue.cluster_id, 1, {"present_value"}),
Expand All @@ -199,13 +199,13 @@ async def poll_control_device_mock(zha_gateway: Gateway) -> Device:
(zigpy.zcl.clusters.general.MultistateInput.cluster_id, 1, {"present_value"}),
(zigpy.zcl.clusters.general.MultistateOutput.cluster_id, 1, {"present_value"}),
(zigpy.zcl.clusters.general.MultistateValue.cluster_id, 1, {"present_value"}),
(zigpy.zcl.clusters.general.Commissioning.cluster_id, 1, {}),
(zigpy.zcl.clusters.general.Partition.cluster_id, 1, {}),
(zigpy.zcl.clusters.general.Ota.cluster_id, 0, {}),
(zigpy.zcl.clusters.general.PowerProfile.cluster_id, 1, {}),
(zigpy.zcl.clusters.general.ApplianceControl.cluster_id, 1, {}),
(zigpy.zcl.clusters.general.PollControl.cluster_id, 1, {}),
(zigpy.zcl.clusters.general.GreenPowerProxy.cluster_id, 0, {}),
(zigpy.zcl.clusters.general.Commissioning.cluster_id, 1, set()),
(zigpy.zcl.clusters.general.Partition.cluster_id, 1, set()),
(zigpy.zcl.clusters.general.Ota.cluster_id, 0, set()),
(zigpy.zcl.clusters.general.PowerProfile.cluster_id, 1, set()),
(zigpy.zcl.clusters.general.ApplianceControl.cluster_id, 1, set()),
(zigpy.zcl.clusters.general.PollControl.cluster_id, 1, set()),
(zigpy.zcl.clusters.general.GreenPowerProxy.cluster_id, 0, set()),
(zigpy.zcl.clusters.closures.DoorLock.cluster_id, 1, {"lock_state"}),
(
zigpy.zcl.clusters.hvac.Thermostat.cluster_id,
Expand Down Expand Up @@ -285,6 +285,15 @@ async def poll_control_device_mock(zha_gateway: Gateway) -> Device:
zigpy.zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id,
1,
{
"ac_frequency",
"ac_voltage_divisor",
"ac_current_divisor",
"ac_power_divisor",
"ac_voltage_multiplier",
"ac_power_multiplier",
"power_divisor",
"power_multiplier",
"ac_current_multiplier",
"ac_frequency",
"active_power",
"active_power_ph_b",
Expand Down Expand Up @@ -332,15 +341,15 @@ async def test_in_cluster_handler_config(
assert cluster_handler.status == ClusterHandlerStatus.CONFIGURED

assert cluster.bind.call_count == bind_count

reported_attrs = set()

for mock_call in cluster.configure_reporting_multiple.mock_calls:
reported_attrs.update(mock_call.args[0].keys())

assert attrs == reported_attrs
assert cluster.configure_reporting.call_count == 0
assert cluster.configure_reporting_multiple.call_count == math.ceil(len(attrs) / 3)
reported_attrs = {
a
for a in attrs
for attr in cluster.configure_reporting_multiple.call_args_list
for attrs in attr[0][0]
}
assert set(attrs) == reported_attrs


async def test_cluster_handler_bind_error(
Expand Down
8 changes: 8 additions & 0 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,14 @@ async def test_elec_measurement_skip_unsupported_attribute(
"power_factor_ph_c",
"ac_frequency",
"ac_frequency_max",
"ac_voltage_divisor",
"ac_current_divisor",
"ac_power_divisor",
"ac_voltage_multiplier",
"ac_power_multiplier",
"ac_current_multiplier",
"power_divisor",
"power_multiplier",
}
for attr in all_attrs - supported_attributes:
cluster.add_unsupported_attribute(attr)
Expand Down
32 changes: 20 additions & 12 deletions zha/application/platforms/sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ def state(self) -> dict[str, Any]:
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
stop_on_match_group=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
models={"VZM31-SN", "SP 234", "outletv4"},
models={"VZM31-SN", "SP 234", "outletv4", "INSPELNING Smart plug"},
)
class ElectricalMeasurement(PollableSensor):
"""Active power measurement."""
Expand All @@ -626,8 +626,9 @@ class ElectricalMeasurement(PollableSensor):
_attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement: str = UnitOfPower.WATT
_divisor_attribute_name: str | None = "ac_power_divisor"
_multiplier_attribute_name: str | None = "ac_power_multiplier"
_attr_max_attribute_name: str = None
_div_mul_prefix: str | None = "ac_power"

def __init__(
self,
Expand Down Expand Up @@ -667,14 +668,16 @@ def state(self) -> dict[str, Any]:

def formatter(self, value: int) -> int | float:
"""Return 'normalized' value."""
if self._div_mul_prefix:
multiplier = getattr(
self._cluster_handler, f"{self._div_mul_prefix}_multiplier"
)
divisor = getattr(self._cluster_handler, f"{self._div_mul_prefix}_divisor")
if self._multiplier_attribute_name:
multiplier = getattr(self._cluster_handler, self._multiplier_attribute_name)
else:
multiplier = self._multiplier

if self._divisor_attribute_name:
divisor = getattr(self._cluster_handler, self._divisor_attribute_name)
else:
divisor = self._divisor

value = float(value * multiplier) / divisor
if value < 100 and divisor > 1:
return round(value, self._decimals)
Expand Down Expand Up @@ -724,7 +727,8 @@ class ElectricalMeasurementApparentPower(PolledElectricalMeasurement):
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.APPARENT_POWER
_attr_native_unit_of_measurement = UnitOfApparentPower.VOLT_AMPERE
_div_mul_prefix = "ac_power"
_divisor_attribute_name = "ac_power_divisor"
_multiplier_attribute_name = "ac_power_multiplier"


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
Expand All @@ -736,7 +740,8 @@ class ElectricalMeasurementRMSCurrent(PolledElectricalMeasurement):
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT
_attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE
_div_mul_prefix = "ac_current"
_divisor_attribute_name = "ac_current_divisor"
_multiplier_attribute_name = "ac_current_multiplier"


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
Expand Down Expand Up @@ -770,7 +775,8 @@ class ElectricalMeasurementRMSVoltage(PolledElectricalMeasurement):
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLTAGE
_attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
_div_mul_prefix = "ac_voltage"
_divisor_attribute_name = "ac_voltage_divisor"
_multiplier_attribute_name = "ac_voltage_multiplier"


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
Expand Down Expand Up @@ -805,7 +811,8 @@ class ElectricalMeasurementFrequency(PolledElectricalMeasurement):
_attr_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY
_attr_translation_key: str = "ac_frequency"
_attr_native_unit_of_measurement = UnitOfFrequency.HERTZ
_div_mul_prefix = "ac_frequency"
_divisor_attribute_name = "ac_frequency_divisor"
_multiplier_attribute_name = "ac_frequency_multiplier"


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
Expand All @@ -817,7 +824,8 @@ class ElectricalMeasurementPowerFactor(PolledElectricalMeasurement):
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR
_attr_native_unit_of_measurement = PERCENTAGE
_div_mul_prefix = None
_divisor_attribute_name = None
_multiplier_attribute_name = None


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
Expand Down
41 changes: 33 additions & 8 deletions zha/zigbee/cluster_handlers/homeautomation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from zha.zigbee.cluster_handlers import AttrReportConfig, ClusterHandler, registries
from zha.zigbee.cluster_handlers.const import (
CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
REPORT_CONFIG_IMMEDIATE,
REPORT_CONFIG_OP,
)

Expand Down Expand Up @@ -60,6 +61,38 @@ class MeasurementType(enum.IntFlag):
POWER_QUALITY_MEASUREMENT = 256

REPORT_CONFIG = (
AttrReportConfig(
attr=ElectricalMeasurement.AttributeDefs.ac_voltage_multiplier.name,
config=REPORT_CONFIG_IMMEDIATE,
),
AttrReportConfig(
attr=ElectricalMeasurement.AttributeDefs.ac_voltage_divisor.name,
config=REPORT_CONFIG_IMMEDIATE,
),
AttrReportConfig(
attr=ElectricalMeasurement.AttributeDefs.ac_current_multiplier.name,
config=REPORT_CONFIG_IMMEDIATE,
),
AttrReportConfig(
attr=ElectricalMeasurement.AttributeDefs.ac_current_divisor.name,
config=REPORT_CONFIG_IMMEDIATE,
),
AttrReportConfig(
attr=ElectricalMeasurement.AttributeDefs.ac_power_multiplier.name,
config=REPORT_CONFIG_IMMEDIATE,
),
AttrReportConfig(
attr=ElectricalMeasurement.AttributeDefs.ac_power_divisor.name,
config=REPORT_CONFIG_IMMEDIATE,
),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put these at the very beginning just in case the device's attribute reports are sent in the order of the reporting config. If this is the case, you may need to re-join the device to get this to work.

AttrReportConfig(
attr=ElectricalMeasurement.AttributeDefs.power_multiplier.name,
config=REPORT_CONFIG_IMMEDIATE,
),
AttrReportConfig(
attr=ElectricalMeasurement.AttributeDefs.power_divisor.name,
config=REPORT_CONFIG_IMMEDIATE,
),
AttrReportConfig(
attr=ElectricalMeasurement.AttributeDefs.active_power.name,
config=REPORT_CONFIG_OP,
Expand Down Expand Up @@ -132,24 +165,16 @@ class MeasurementType(enum.IntFlag):
ElectricalMeasurement.AttributeDefs.rms_voltage_max_ph_c.name,
]
ZCL_INIT_ATTRS = {
ElectricalMeasurement.AttributeDefs.ac_current_divisor.name: True,
ElectricalMeasurement.AttributeDefs.ac_current_multiplier.name: True,
ElectricalMeasurement.AttributeDefs.ac_frequency_divisor.name: True,
ElectricalMeasurement.AttributeDefs.ac_frequency_max.name: True,
ElectricalMeasurement.AttributeDefs.ac_frequency_multiplier.name: True,
ElectricalMeasurement.AttributeDefs.ac_power_divisor.name: True,
ElectricalMeasurement.AttributeDefs.ac_power_multiplier.name: True,
ElectricalMeasurement.AttributeDefs.ac_voltage_divisor.name: True,
ElectricalMeasurement.AttributeDefs.ac_voltage_multiplier.name: True,
ElectricalMeasurement.AttributeDefs.active_power_max.name: True,
ElectricalMeasurement.AttributeDefs.active_power_max_ph_b.name: True,
ElectricalMeasurement.AttributeDefs.active_power_max_ph_c.name: True,
ElectricalMeasurement.AttributeDefs.measurement_type.name: True,
ElectricalMeasurement.AttributeDefs.power_divisor.name: True,
ElectricalMeasurement.AttributeDefs.power_factor.name: True,
ElectricalMeasurement.AttributeDefs.power_factor_ph_b.name: True,
ElectricalMeasurement.AttributeDefs.power_factor_ph_c.name: True,
ElectricalMeasurement.AttributeDefs.power_multiplier.name: True,
ElectricalMeasurement.AttributeDefs.rms_current_max.name: True,
ElectricalMeasurement.AttributeDefs.rms_current_max_ph_b.name: True,
ElectricalMeasurement.AttributeDefs.rms_current_max_ph_c.name: True,
Expand Down
Loading