Skip to content

Commit 8fd7bc5

Browse files
authored
Implement quirks v2 attribute_converter (#360)
1 parent cb615d7 commit 8fd7bc5

File tree

4 files changed

+119
-2
lines changed

4 files changed

+119
-2
lines changed

tests/test_binary_sensor.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
from unittest.mock import MagicMock, call
55

66
import pytest
7+
from zigpy.profiles import zha
78
import zigpy.profiles.zha
9+
from zigpy.quirks import DeviceRegistry
10+
from zigpy.quirks.v2 import CustomDeviceV2, QuirkBuilder
811
from zigpy.zcl.clusters import general, measurement, security
12+
from zigpy.zcl.clusters.general import OnOff
913

1014
from tests.common import (
1115
SIG_EP_INPUT,
@@ -22,7 +26,12 @@
2226
from zha.application import Platform
2327
from zha.application.gateway import Gateway
2428
from zha.application.platforms import PlatformEntity
25-
from zha.application.platforms.binary_sensor import Accelerometer, IASZone, Occupancy
29+
from zha.application.platforms.binary_sensor import (
30+
Accelerometer,
31+
BinarySensor,
32+
IASZone,
33+
Occupancy,
34+
)
2635
from zha.zigbee.cluster_handlers.const import SMARTTHINGS_ACCELERATION_CLUSTER
2736

2837
DEVICE_IAS = {
@@ -201,3 +210,49 @@ async def test_smarttthings_multi(
201210
{"attribute_id": 18, "attribute_name": "x_axis", "attribute_value": 120},
202211
)
203212
]
213+
214+
215+
async def test_quirks_binary_sensor_attr_converter(zha_gateway: Gateway) -> None:
216+
"""Test ZHA quirks v2 binary_sensor with attribute_converter."""
217+
218+
registry = DeviceRegistry()
219+
zigpy_dev = create_mock_zigpy_device(
220+
zha_gateway,
221+
{
222+
1: {
223+
SIG_EP_INPUT: [general.Basic.cluster_id, general.OnOff.cluster_id],
224+
SIG_EP_OUTPUT: [],
225+
SIG_EP_TYPE: zha.DeviceType.SIMPLE_SENSOR,
226+
}
227+
},
228+
manufacturer="manufacturer",
229+
model="model",
230+
)
231+
232+
(
233+
QuirkBuilder(zigpy_dev.manufacturer, zigpy_dev.model, registry=registry)
234+
.binary_sensor(
235+
OnOff.AttributeDefs.on_off.name,
236+
OnOff.cluster_id,
237+
translation_key="on_off",
238+
fallback_name="On/off",
239+
attribute_converter=lambda x: not bool(x), # invert value with lambda
240+
)
241+
.add_to_registry()
242+
)
243+
244+
zigpy_device_ = registry.get_device(zigpy_dev)
245+
246+
assert isinstance(zigpy_device_, CustomDeviceV2)
247+
cluster = zigpy_device_.endpoints[1].on_off
248+
249+
zha_device = await join_zigpy_device(zha_gateway, zigpy_device_)
250+
entity = get_entity(zha_device, platform=Platform.BINARY_SENSOR)
251+
assert isinstance(entity, BinarySensor)
252+
253+
# send updated value, check if the value is inverted
254+
await send_attributes_report(zha_gateway, cluster, {"on_off": 1})
255+
assert entity.is_on is False
256+
257+
await send_attributes_report(zha_gateway, cluster, {"on_off": 0})
258+
assert entity.is_on is True

tests/test_sensor.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@
1111
import pytest
1212
from zhaquirks.danfoss import thermostat as danfoss_thermostat
1313
from zigpy.device import Device as ZigpyDevice
14+
from zigpy.profiles import zha
1415
import zigpy.profiles.zha
15-
from zigpy.quirks import CustomCluster, get_device
16+
from zigpy.quirks import CustomCluster, DeviceRegistry, get_device
1617
from zigpy.quirks.v2 import CustomDeviceV2, QuirkBuilder, ReportingConfig
1718
from zigpy.quirks.v2.homeassistant.sensor import (
1819
SensorDeviceClass as SensorDeviceClassV2,
1920
)
2021
import zigpy.types as t
2122
from zigpy.zcl import Cluster
2223
from zigpy.zcl.clusters import general, homeautomation, hvac, measurement, smartenergy
24+
from zigpy.zcl.clusters.general import AnalogInput
2325
from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster
2426

2527
from tests.common import (
@@ -1679,3 +1681,51 @@ async def test_danfoss_thermostat_sw_error(zha_gateway: Gateway) -> None:
16791681
assert entity.extra_state_attribute_names
16801682
assert "Top_pcb_sensor_error" in entity.extra_state_attribute_names
16811683
assert entity.state["Top_pcb_sensor_error"]
1684+
1685+
1686+
async def test_quirks_sensor_attr_converter(zha_gateway: Gateway) -> None:
1687+
"""Test ZHA quirks v2 sensor with attribute_converter."""
1688+
1689+
registry = DeviceRegistry()
1690+
zigpy_dev = create_mock_zigpy_device(
1691+
zha_gateway,
1692+
{
1693+
1: {
1694+
SIG_EP_INPUT: [
1695+
general.Basic.cluster_id,
1696+
general.AnalogInput.cluster_id,
1697+
],
1698+
SIG_EP_OUTPUT: [],
1699+
SIG_EP_TYPE: zha.DeviceType.SIMPLE_SENSOR,
1700+
}
1701+
},
1702+
manufacturer="manufacturer",
1703+
model="model",
1704+
)
1705+
1706+
(
1707+
QuirkBuilder(zigpy_dev.manufacturer, zigpy_dev.model, registry=registry)
1708+
.sensor(
1709+
AnalogInput.AttributeDefs.present_value.name,
1710+
AnalogInput.cluster_id,
1711+
translation_key="quirks_sensor",
1712+
fallback_name="Quirks sensor",
1713+
attribute_converter=lambda x: x + 100,
1714+
)
1715+
.add_to_registry()
1716+
)
1717+
1718+
zigpy_device_ = registry.get_device(zigpy_dev)
1719+
1720+
assert isinstance(zigpy_device_, CustomDeviceV2)
1721+
cluster = zigpy_device_.endpoints[1].analog_input
1722+
1723+
zha_device = await join_zigpy_device(zha_gateway, zigpy_device_)
1724+
entity = get_entity(zha_device, platform=Platform.SENSOR, qualifier="present_value")
1725+
1726+
# send updated value, check if the value is converted
1727+
await send_attributes_report(zha_gateway, cluster, {"present_value": 100})
1728+
assert entity.state["state"] == 200.0
1729+
1730+
await send_attributes_report(zha_gateway, cluster, {"present_value": 0})
1731+
assert entity.state["state"] == 100.0

zha/application/platforms/binary_sensor/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from dataclasses import dataclass
66
import functools
77
import logging
8+
import typing
89
from typing import TYPE_CHECKING
910

1011
from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT
@@ -59,6 +60,7 @@ class BinarySensor(PlatformEntity):
5960

6061
_attr_device_class: BinarySensorDeviceClass | None
6162
_attribute_name: str
63+
_attribute_converter: typing.Callable[[typing.Any], typing.Any] | None = None
6264
PLATFORM: Platform = Platform.BINARY_SENSOR
6365

6466
def __init__(
@@ -82,6 +84,8 @@ def _init_from_quirks_metadata(self, entity_metadata: BinarySensorMetadata) -> N
8284
"""Init this entity from the quirks metadata."""
8385
super()._init_from_quirks_metadata(entity_metadata)
8486
self._attribute_name = entity_metadata.attribute_name
87+
if entity_metadata.attribute_converter is not None:
88+
self._attribute_converter = entity_metadata.attribute_converter
8589
if entity_metadata.device_class is not None:
8690
self._attr_device_class = validate_device_class(
8791
BinarySensorDeviceClass,
@@ -113,6 +117,8 @@ def is_on(self) -> bool:
113117
)
114118
if raw_state is None:
115119
return False
120+
if self._attribute_converter:
121+
return self._attribute_converter(raw_state)
116122
return self.parse(raw_state)
117123

118124
def handle_cluster_handler_attribute_updated(

zha/application/platforms/sensor/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import functools
1010
import logging
1111
import numbers
12+
import typing
1213
from typing import TYPE_CHECKING, Any, Self
1314

1415
from zhaquirks.danfoss import thermostat as danfoss_thermostat
@@ -150,6 +151,7 @@ class Sensor(PlatformEntity):
150151

151152
PLATFORM = Platform.SENSOR
152153
_attribute_name: int | str | None = None
154+
_attribute_converter: typing.Callable[[typing.Any], typing.Any] | None = None
153155
_decimals: int = 1
154156
_divisor: int = 1
155157
_multiplier: int | float = 1
@@ -226,6 +228,8 @@ def _init_from_quirks_metadata(self, entity_metadata: ZCLSensorMetadata) -> None
226228
"""Init this entity from the quirks metadata."""
227229
super()._init_from_quirks_metadata(entity_metadata)
228230
self._attribute_name = entity_metadata.attribute_name
231+
if entity_metadata.attribute_converter is not None:
232+
self._attribute_converter = entity_metadata.attribute_converter
229233
if entity_metadata.divisor is not None:
230234
self._divisor = entity_metadata.divisor
231235
if entity_metadata.multiplier is not None:
@@ -275,6 +279,8 @@ def native_value(self) -> date | datetime | str | int | float | None:
275279
raw_state = self._cluster_handler.cluster.get(self._attribute_name)
276280
if raw_state is None:
277281
return None
282+
if self._attribute_converter:
283+
return self._attribute_converter(raw_state)
278284
return self.formatter(raw_state)
279285

280286
def handle_cluster_handler_attribute_updated(

0 commit comments

Comments
 (0)