Repository: zigpy/zha-device-handlers
Pull request #3718 (2025-01-16 19:45:41)

Add illuminance method to `TuyaQuirkBuilder`
## Proposed change
<!--
  Explain your proposed change below.
-->


- Adds `tuya_illuminance` method to `TuyaQuirkBuilder` 
- Refactored `_TZE200_pay2byax` quirk to use the new quirk builder method
- 
## Additional information
<!--
  Please include any additional information that is important to this PR.
  For example, if this PR is a potentially breaking change, mention that here.
  If this PR requires other PRs to be merged in HA Core or other projects, mention that.
  Lastly, if this PR fixes a specific issue, please include "Fixes #xxxx".
-->


## Checklist
<!--
  Put an 'x' in all boxes that apply.
  Note: You do not need to tick all boxes before creating a PR.
-->

- [x] The changes are tested and work correctly
- [ ] `pre-commit` checks pass / the code has been formatted using Black
- [ ] Tests have been added to verify that the new code works


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3718?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 89.88%. Comparing base [(`e61ecbb`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/e61ecbb4f64291c663fd19d23f95f7db5d33e1f0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) to head [(`4c1627e`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/4c1627eb47f6fc2372d8cbefe1a5c443def1db2b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##              dev    #3718   +/-   ##
=======================================
  Coverage   89.87%   89.88%           
=======================================
  Files         320      320           
  Lines       10381    10386    +5     
=======================================
+ Hits         9330     9335    +5     
  Misses       1051     1051           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3718?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).

TheJulianJES: Rebased this due to https://github.com/zigpy/zha-device-handlers/pull/3683

Patch:
diff --git a/zhaquirks/tuya/builder/__init__.py b/zhaquirks/tuya/builder/__init__.py
index b4a1f13..5f64aef 100644
--- a/zhaquirks/tuya/builder/__init__.py
+++ b/zhaquirks/tuya/builder/__init__.py
@@ -2,6 +2,7 @@
 
 from collections.abc import Callable
 from enum import Enum
+import math
 from typing import Any, Optional
 
 from zigpy.quirks import _DEVICE_REGISTRY
@@ -18,6 +19,7 @@ from zigpy.zcl.clusters.measurement import (
     PM25,
     CarbonDioxideConcentration,
     FormaldehydeConcentration,
+    IlluminanceMeasurement,
     RelativeHumidity,
     SoilMoisture,
     TemperatureMeasurement,
@@ -161,6 +163,14 @@ class TuyaAirQualityVOC(TuyaLocalCluster):
         )
 
 
+class TuyaIlluminance(IlluminanceMeasurement, TuyaLocalCluster):
+    """Tuya local illuminance cluster."""
+
+    _CONSTANT_ATTRIBUTES = {
+        IlluminanceMeasurement.AttributeDefs.light_sensor_type.id: IlluminanceMeasurement.LightSensorType.Photodiode
+    }
+
+
 class TuyaQuirkBuilder(QuirkBuilder):
     """Tuya QuirkBuilder."""
 
@@ -222,6 +232,24 @@ class TuyaQuirkBuilder(QuirkBuilder):
             dp_id=dp_id, power_cfg=TuyaPowerConfigurationClusterBattery, scale=scale
         )
 
+    def tuya_illuminance(
+        self,
+        dp_id: int,
+        illuminance_cfg: TuyaLocalCluster = TuyaIlluminance,
+        converter: Optional[Callable[[Any], Any]] = (
+            lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0
+        ),
+    ) -> QuirkBuilder:
+        """Add a Tuya Illuminance Configuration."""
+        self.tuya_dp(
+            dp_id,
+            illuminance_cfg.ep_attribute,
+            IlluminanceMeasurement.AttributeDefs.measured_value.name,
+            converter=converter,
+        )
+        self.adds(illuminance_cfg)
+        return self
+
     def tuya_contact(self, dp_id: int):
         """Add a Tuya IAS contact sensor."""
         self.tuya_ias(
diff --git a/zhaquirks/tuya/ts0601_motion.py b/zhaquirks/tuya/ts0601_motion.py
index b4df9dd..e739370 100644
--- a/zhaquirks/tuya/ts0601_motion.py
+++ b/zhaquirks/tuya/ts0601_motion.py
@@ -1,7 +1,6 @@
 """BlitzWolf IS-3/Tuya motion rechargeable occupancy sensor."""
 
 import asyncio
-import math
 from typing import Any
 
 from zigpy.quirks.v2 import EntityPlatform, EntityType
@@ -9,17 +8,13 @@ from zigpy.quirks.v2.homeassistant import UnitOfLength, UnitOfTime
 from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass
 from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass
 import zigpy.types as t
-from zigpy.zcl.clusters.measurement import IlluminanceMeasurement, OccupancySensing
+from zigpy.zcl.clusters.measurement import OccupancySensing
 from zigpy.zcl.clusters.security import IasZone
 
 from zhaquirks.tuya import TuyaLocalCluster
 from zhaquirks.tuya.builder import TuyaQuirkBuilder
 
 
-class TuyaIlluminanceCluster(IlluminanceMeasurement, TuyaLocalCluster):
-    """Tuya Illuminance cluster."""
-
-
 class TuyaOccupancySensing(OccupancySensing, TuyaLocalCluster):
     """Tuya local OccupancySensing cluster."""
 
@@ -207,7 +202,6 @@ base_tuya_motion = (
         translation_key="distance",
         fallback_name="Target distance",
     )
-    .adds(TuyaIlluminanceCluster)
     .skip_configuration()
 )
 
@@ -241,12 +235,7 @@ base_tuya_motion = (
         translation_key="presence_sensitivity",
         fallback_name="Presence sensitivity",
     )
-    .tuya_dp(
-        dp_id=103,
-        ep_attribute=TuyaIlluminanceCluster.ep_attribute,
-        attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name,
-        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
-    )
+    .tuya_illuminance(dp_id=103)
     .tuya_number(
         dp_id=105,
         attribute_name="presence_timeout",
@@ -315,12 +304,7 @@ base_tuya_motion = (
         translation_key="presence_sensitivity",
         fallback_name="Presence sensitivity",
     )
-    .tuya_dp(
-        dp_id=104,
-        ep_attribute=TuyaIlluminanceCluster.ep_attribute,
-        attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name,
-        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
-    )
+    .tuya_illuminance(dp_id=104)
     # 103 cli, z2m lists as not working
     .add_to_registry()
 )
@@ -363,12 +347,7 @@ base_tuya_motion = (
         translation_key="fading_time",
         fallback_name="Fading time",
     )
-    .tuya_dp(
-        dp_id=104,
-        ep_attribute=TuyaIlluminanceCluster.ep_attribute,
-        attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name,
-        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
-    )
+    .tuya_illuminance(dp_id=104)
     .add_to_registry()
 )
 
@@ -424,13 +403,7 @@ base_tuya_motion = (
         translation_key="target_distance",
         fallback_name="Target distance",
     )
-    .tuya_dp(
-        dp_id=102,
-        ep_attribute=TuyaIlluminanceCluster.ep_attribute,
-        attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name,
-        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
-    )
-    .adds(TuyaIlluminanceCluster)
+    .tuya_illuminance(dp_id=102)
     .tuya_number(
         dp_id=103,
         attribute_name="fading_time",
@@ -510,13 +483,7 @@ base_tuya_motion = (
         translation_key="target_distance",
         fallback_name="Target distance",
     )
-    .tuya_dp(
-        dp_id=102,
-        ep_attribute=TuyaIlluminanceCluster.ep_attribute,
-        attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name,
-        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
-    )
-    .adds(TuyaIlluminanceCluster)
+    .tuya_illuminance(dp_id=102)
     .tuya_number(
         dp_id=103,
         attribute_name="hold_delay_time",
@@ -686,12 +653,7 @@ base_tuya_motion = (
         fallback_name="Fading time",
     )
     # 103 cline, z2m doesn't expose
-    .tuya_dp(
-        dp_id=104,
-        ep_attribute=TuyaIlluminanceCluster.ep_attribute,
-        attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name,
-        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
-    )
+    .tuya_illuminance(dp_id=104)
     .tuya_number(
         dp_id=105,
         attribute_name="entry_sensitivity",
@@ -937,12 +899,7 @@ base_tuya_motion = (
         attribute_name=OccupancySensing.AttributeDefs.occupancy.name,
         converter=lambda x: x == 1,
     )
-    .tuya_dp(
-        dp_id=101,
-        ep_attribute=TuyaIlluminanceCluster.ep_attribute,
-        attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name,
-        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
-    )
+    .tuya_illuminance(dp_id=101)
     .adds(TuyaOccupancySensing)
     .tuya_switch(
         dp_id=102,
@@ -981,7 +938,6 @@ base_tuya_motion = (
         translation_key="presence_timeout",
         fallback_name="Fade time",
     )
-    .adds(TuyaIlluminanceCluster)
     .skip_configuration()
     .add_to_registry()
 )
@@ -1069,15 +1025,10 @@ base_tuya_motion = (
         translation_key="work_mode",
         fallback_name="Work mode",
     )
-    .tuya_dp(
+    .tuya_illuminance(
         dp_id=102,
-        ep_attribute=TuyaIlluminanceCluster.ep_attribute,
-        attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name,
-        converter=lambda x: {0: 10, 1: 20, 2: 50, 3: 100}[
-            x
-        ],  # z2m has 10lux, 20lux, 50lux, then 24hrs?
+        converter=lambda x: {0: 10, 1: 20, 2: 50, 3: 100}[x],
     )
-    .adds(TuyaIlluminanceCluster)
     .tuya_number(
         dp_id=103,
         attribute_name="output_time",
@@ -1136,13 +1087,7 @@ base_tuya_motion = (
         translation_key="fade_time",
         fallback_name="Fade time",
     )
-    .tuya_dp(
-        dp_id=12,
-        ep_attribute=TuyaIlluminanceCluster.ep_attribute,
-        attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name,
-        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
-    )
-    .adds(TuyaIlluminanceCluster)
+    .tuya_illuminance(dp_id=12)
     .tuya_number(
         dp_id=105,
         attribute_name="illuminance_interval",
@@ -1164,13 +1109,7 @@ base_tuya_motion = (
 (
     TuyaQuirkBuilder("_TZE204_sxm7l9xa", "TS0601")
     .applies_to("_TZE204_e5m9c5hl", "TS0601")
-    .tuya_dp(
-        dp_id=104,
-        ep_attribute=TuyaIlluminanceCluster.ep_attribute,
-        attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name,
-        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
-    )
-    .adds(TuyaIlluminanceCluster)
+    .tuya_illuminance(dp_id=104)
     .tuya_dp(
         dp_id=105,
         ep_attribute=TuyaOccupancySensing.ep_attribute,
@@ -1271,13 +1210,7 @@ base_tuya_motion = (
         translation_key="motion_sensitivity",
         fallback_name="Motion sensitivity",
     )
-    .tuya_dp(
-        dp_id=12,
-        ep_attribute=TuyaIlluminanceCluster.ep_attribute,
-        attribute_name=TuyaIlluminanceCluster.AttributeDefs.measured_value.name,
-        converter=lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0,
-    )
-    .adds(TuyaIlluminanceCluster)
+    .tuya_illuminance(dp_id=12)
     .tuya_number(
         dp_id=101,
         attribute_name="illuminance_interval",
diff --git a/zhaquirks/tuya/tuya_sensor.py b/zhaquirks/tuya/tuya_sensor.py
index 0a19164..fbbdaa8 100644
--- a/zhaquirks/tuya/tuya_sensor.py
+++ b/zhaquirks/tuya/tuya_sensor.py
@@ -4,7 +4,7 @@ import copy
 
 from zigpy.quirks.v2 import EntityPlatform, EntityType
 from zigpy.quirks.v2.homeassistant import PERCENTAGE, UnitOfTemperature, UnitOfTime
-from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass
+from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass
 import zigpy.types as t
 from zigpy.zcl import foundation
 
@@ -304,14 +304,7 @@ class NoManufTimeTuyaMCUCluster(TuyaMCUCluster):
 (
     TuyaQuirkBuilder("_TZE200_pay2byax", "TS0601")  # Cusam ZG-102ZL
     .applies_to("_TZE200_n8dljorx", "TS0601")
-    .tuya_sensor(
-        dp_id=101,
-        attribute_name="measured_value",
-        type=t.uint16_t,
-        fallback_name="Illuminance",
-        device_class=SensorDeviceClass.ILLUMINANCE,
-        state_class=SensorStateClass.MEASUREMENT,
-    )
+    .tuya_illuminance(dp_id=101)
     .tuya_contact(dp_id=1)
     .tuya_battery(dp_id=2)
     .skip_configuration()


Test patch:
diff --git a/tests/test_tuya_builder.py b/tests/test_tuya_builder.py
index e82c023..425c7ac 100644
--- a/tests/test_tuya_builder.py
+++ b/tests/test_tuya_builder.py
@@ -25,6 +25,7 @@ from zhaquirks.tuya.builder import (
     TuyaIasContact,
     TuyaIasFire,
     TuyaIasGas,
+    TuyaIlluminance,
     TuyaPM25Concentration,
     TuyaQuirkBuilder,
     TuyaRelativeHumidity,
@@ -60,6 +61,7 @@ zhaquirks.setup()
             TuyaFormaldehydeConcentration,
         ),
         ("tuya_gas", "ias_zone", TuyaIasGas),
+        ("tuya_illuminance", "illuminance", TuyaIlluminance),
     ],
 )
 async def test_convenience_methods(device_mock, method_name, attr_name, exp_class):