Repository: zigpy/zha-device-handlers
Pull request #3504 (2024-11-10 13:41:55)

Fix Sinope device triggers
## Proposed change
Fixes device automation triggers for the Sinope light quirk. The current definition of the automation triggers
uses the `ButtonAction` enum in the `value` field, which throws an error from Voluptuous when passed into HA's `async_attach_trigger`.

## Additional information
Not a breaking change and doesn't require anything else to be merged into HA Core.

## Checklist
- [x] The changes are tested and work correctly
- [x] `pre-commit` checks pass / the code has been formatted using Black
- [x] Tests have been added to verify that the new code works


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3504?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.44%. Comparing base [(`728ee42`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/728ee427263d89a9c9186eacefc90e4994d81b82?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) to head [(`254f9fa`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/254f9fa344383ee219f7f6824ee69a7c183582a8?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    #3504   +/-   ##
=======================================
  Coverage   89.44%   89.44%           
=======================================
  Files         311      311           
  Lines       10033    10033           
=======================================
  Hits         8974     8974           
  Misses       1059     1059           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3504?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).

ckm2k1: I noticed an issue while trying to debug this problem.

To validate my fix, I added a modified copy of the `sinope` quirk into the custom quirks directory in my live HA instance.
Although ZHA loads the modified quirk just fine, since the fix itself happens at definition time, my modifications are loaded too late in the process to fix the call to `async_attach_trigger` in `/zha/core/homeassistant/components/zha/device_trigger.py`. 

Instead, I had to ssh into HA OS as root and modify the `zhaquirks` code manually in `/usr/local/lib/python3.12/site-packages/zhaquirks/sinope/__init__.py`. This worked, but is pretty tedious if you're trying to iterate and check your changes. 

I don't know if it's possible, but it would be nice if the loading order was changed such that if a custom quirk is present and it overrides an existing quirk, it would be loaded _instead_ of the builtin one, and not after it.
ckm2k1: @bassdr a couple questions for you: 

* Does the above comment sound like what you ran into here: https://github.com/zigpy/zha-device-handlers/pull/3438#discussion_r1815884198? If so, how did you work around it?
* Did all your button related automations also break with HA 2024.11.0?

bassdr: > @ckm2k1 
> https://github.com/zigpy/zha-device-handlers/pull/3504#issuecomment-2466749623
> I don't know if it's possible, but it would be nice if the loading order was changed such that if a custom quirk is present and it overrides an existing quirk, it would be loaded instead of the builtin one, and not after it.


> @bassdr a couple questions for you:
> - Does the above comment sound like what you ran into here: https://github.com/zigpy/zha-device-handlers/pull/3438#discussion_r1815884198? If so, how did you work around it?

Yes, this problem is exactly why I used relative import paths. This way, you can add the __init__.py file to your quirk and it works just fine. See https://github.com/zigpy/zha-device-handlers/pull/3438/commits/f922d1439e2665d0a23a290a3e75017ff0544d1a, actually, you want to revert this one.
So in short, in your config folder called zhaquirks, inside you create a folder named sinope, and you setup your configuration.yaml to point to the zharquirks folder (not the sinope folder). The sinope folder must include the __init__.py and all files that will be affected by the changes. You then have to change everywhere you `import zhaquirks.sinope` to `import .`. It just works. I did not understand the point of having absolute paths but, not the end of the world either...

> - Did all your button related automations also break with HA 2024.11.0?

I haven't tried 2024.11.0 yet, I keep my non-dev HA setup on the last version of last month... But I'm surprised to see this PR indeed, I'll upgrade to check when I get the chance. Do you still have the exact voluptuous error it gave you? Or maybe a snippet of you automation that is giving the error? I'm personally listening to the zha event directly like this:

``` yaml
  - trigger: event
    event_type: zha_event
    event_data:
      endpoint_id: 1
      cluster_id: 65281
      args:
        attribute_name: action_report
```

To test it's the right device:
```yaml
condition: template
value_template: |-
  {{
    device_id('light.upstairslightswitch_lumiere') == trigger.event.data.device_id
  }}
```

and eventually:
``` yaml
choose:
  - conditions:
      - condition: template
        value_template: "{{ trigger.event.data.command == 'initial_press' }}"
    sequence:
      - action: zha.issue_zigbee_cluster_command
        data:
          ieee: |-
            {{
              device_attr('light.upstairslightswitch_lumiere', 'identifiers')
                | selectattr(0,'eq','zha')
                | map(attribute=1)
                | first
            }}
          endpoint_id: 1
          cluster_id: 8
          cluster_type: in
          command: 5
          command_type: server
          params:
            move_mode: |-
              {{
                iif (trigger.event.data.args.button == 'turn_on', 'Up', 'Down')
              }}
            rate: 50
    alias: Initial Press
```



bassdr: I can't find the schema of `device_automation_triggers`, TBH that might be the part I missed in the original PR, but I assumed this was not fixed anywhere.
ckm2k1: @bassdr 
> I haven't tried 2024.11.0 yet, I keep my non-dev HA setup on the last version of last month... But I'm surprised to see this PR indeed, I'll upgrade to check when I get the chance. Do you still have the exact voluptuous error it gave you? Or maybe a snippet of you automation that is giving the error? I'm personally listening to the zha event directly like this:
> 
> ```yaml
>   - trigger: event
>     event_type: zha_event
>     event_data:
>       endpoint_id: 1
>       cluster_id: 65281
>       args:
>         attribute_name: action_report
> ```
> ...

I'm not sure the above is needed anymore after these PRs: https://github.com/zigpy/zha-device-handlers/pull/3313 and https://github.com/zigpy/zha/pull/153. 

I got tired of writing YAML for these things (I have a whole bunch of Sinope devices, particularly light switches), so I submitted the PRs above to patch things and make it work through the UI. Here's a partial example of an automation I'm running:

<img width="1167" alt="image" src="https://github.com/user-attachments/assets/e55bb963-42c5-4581-b170-c8a77b6b2bce">

TheJulianJES: > I don't know if it's possible, but it would be nice if the loading order was changed such that if a custom quirk is present and it overrides an existing quirk, it would be loaded instead of the builtin one, and not after it.

> If all fields in device automation triggers were either validated at dev time, or better yet, automatically coerced into voluptuous supported types, quirk authors wouldn't be forced to use scalar types instead of Enum values.

Can you create an issue for each of these please? You can just copy paste your comment in those. For the first one, maybe in the ZHA repo? For the second one, we should add probably add some basic tests in the quirks repo to validate device triggers IMO. 
ckm2k1: > > I don't know if it's possible, but it would be nice if the loading order was changed such that if a custom quirk is present and it overrides an existing quirk, it would be loaded instead of the builtin one, and not after it.
> 
> > If all fields in device automation triggers were either validated at dev time, or better yet, automatically coerced into voluptuous supported types, quirk authors wouldn't be forced to use scalar types instead of Enum values.
> 
> Can you create an issue for each of these please? You can just copy paste your comment in those. For the first one, maybe in the ZHA repo? For the second one, we should add probably add some basic tests in the quirks repo to validate device triggers IMO.

@TheJulianJES - no problem, will do!

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a52ccd3..6dbd217 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -17,7 +17,7 @@ repos:
       - id: mypy
   
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.7.3
+    rev: v0.7.4
     hooks:
       - id: ruff
         args: ["--fix", "--exit-non-zero-on-fix", "--config", "pyproject.toml"]
diff --git a/README.md b/README.md
index 6539114..43828ff 100644
--- a/README.md
+++ b/README.md
@@ -397,7 +397,7 @@ replacement = {
 
 You can see that we have replaced `ElectricalMeasurement.cluster_id` from endpoint 1 in the `signature` dict with the name of our cluster that we created: `ElectricalMeasurementCluster` and on endpoint 2 we replaced `AnalogInput.cluster_id` with the implementation we created for that: `AnalogInputCluster`. This instructs Zigpy to use these `CustomCluster` derivatives instead of the normal cluster definitions for these clusters and this is why this part of the quirk is called `replacement`.
 
-Now lets put this all together. If you examine the device definition above you will see that we have defined our custom device, we defined the `signature` dict where we transcribed the `SimpleDescriptor` output we obtained when the device joined the network and we defined the `replacement` dict where we swapped the cluster ids for the culsters that we wanted to replace with the `CustomCluster` implementations that we created.
+Now lets put this all together. If you examine the device definition above you will see that we have defined our custom device, we defined the `signature` dict where we transcribed the `SimpleDescriptor` output we obtained when the device joined the network and we defined the `replacement` dict where we swapped the cluster ids for the clusters that we wanted to replace with the `CustomCluster` implementations that we created.
 
 # Contribution Guidelines
 
diff --git a/zhaquirks/philips/rom001.py b/zhaquirks/philips/rom001.py
index 0588ebc..2996d65 100644
--- a/zhaquirks/philips/rom001.py
+++ b/zhaquirks/philips/rom001.py
@@ -65,7 +65,7 @@ class PhilipsROM001(CustomDevice):
         #  device_version=1
         #  input_clusters=[0, 1, 3, 64512, 4096]
         #  output_clusters=[25, 0, 3, 4, 6, 8, 5, 4096]>
-        MODELS_INFO: [(PHILIPS, "ROM001"), (SIGNIFY, "ROM001")],
+        MODELS_INFO: [(PHILIPS, "ROM001"), (SIGNIFY, "ROM001"), (SIGNIFY, "RDM003")],
         ENDPOINTS: {
             1: {
                 PROFILE_ID: zha.PROFILE_ID,
diff --git a/zhaquirks/schneiderelectric/__init__.py b/zhaquirks/schneiderelectric/__init__.py
index 10981ad..7efb815 100644
--- a/zhaquirks/schneiderelectric/__init__.py
+++ b/zhaquirks/schneiderelectric/__init__.py
@@ -167,10 +167,10 @@ class SEBallast(CustomCluster, Ballast):
             id=0xE000, type=SEControlMode, is_manufacturer_specific=True
         )
         se_wiring_mode: Final = ZCLAttributeDef(
-            id=0xE002, type=t.enum8, is_manufacturer_specific=True
+            id=0xE002, type=SEWiringMode, is_manufacturer_specific=True
         )
         se_dimming_curve: Final = ZCLAttributeDef(
-            id=0xE002, type=t.enum8, is_manufacturer_specific=True
+            id=0xE002, type=SEDimmingCurve, is_manufacturer_specific=True
         )
 
 
diff --git a/zhaquirks/schneiderelectric/dimmers.py b/zhaquirks/schneiderelectric/dimmers.py
index e7803b3..4621b14 100644
--- a/zhaquirks/schneiderelectric/dimmers.py
+++ b/zhaquirks/schneiderelectric/dimmers.py
@@ -1,129 +1,33 @@
 """Schneider Electric dimmers and switches quirks."""
 
-from zigpy.profiles import zgp, zha
-from zigpy.quirks import CustomDevice
-from zigpy.zcl.clusters.closures import WindowCovering
-from zigpy.zcl.clusters.general import (
-    Basic,
-    GreenPowerProxy,
-    Groups,
-    Identify,
-    LevelControl,
-    OnOff,
-    Ota,
-    Scenes,
-)
-from zigpy.zcl.clusters.homeautomation import Diagnostic
-from zigpy.zcl.clusters.lighting import Ballast
+from zigpy.quirks.v2 import QuirkBuilder
 
-from zhaquirks.const import (
-    DEVICE_TYPE,
-    ENDPOINTS,
-    INPUT_CLUSTERS,
-    MODELS_INFO,
-    OUTPUT_CLUSTERS,
-    PROFILE_ID,
+from zhaquirks.schneiderelectric import (
+    SE_MANUF_NAME,
+    SEBallast,
+    SEBasic,
+    SEOnOff,
+    SESpecific,
 )
-from zhaquirks.schneiderelectric import SE_MANUF_NAME, SEBallast, SEBasic, SESpecific
-
 
-class NHRotaryDimmer1(CustomDevice):
-    """NHROTARY/DIMMER/1 by Schneider Electric."""
+(
+    QuirkBuilder(SE_MANUF_NAME, "NHROTARY/DIMMER/1")
+    .applies_to(SE_MANUF_NAME, "NHROTARY/UNIDIM/1")
+    .applies_to(SE_MANUF_NAME, "NHPB/DIMMER/1")
+    .applies_to(SE_MANUF_NAME, "NHPB/UNIDIM/1")
+    .replaces(SEBasic, endpoint_id=3)
+    .replaces(SEBallast, endpoint_id=3)
+    .replaces(SEOnOff, endpoint_id=3)
+    .replaces(SEBasic, endpoint_id=21)
+    .replaces(SESpecific, endpoint_id=21)
+    .add_to_registry()
+)
 
-    signature = {
-        MODELS_INFO: [
-            (SE_MANUF_NAME, "NHROTARY/DIMMER/1"),
-        ],
-        ENDPOINTS: {
-            3: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT,
-                INPUT_CLUSTERS: [
-                    Basic.cluster_id,
-                    Identify.cluster_id,
-                    Groups.cluster_id,
-                    Scenes.cluster_id,
-                    OnOff.cluster_id,
-                    LevelControl.cluster_id,
-                    Ballast.cluster_id,
-                    Diagnostic.cluster_id,
-                ],
-                OUTPUT_CLUSTERS: [
-                    Ota.cluster_id,
-                ],
-            },
-            21: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.DIMMER_SWITCH,
-                INPUT_CLUSTERS: [
-                    Basic.cluster_id,
-                    Identify.cluster_id,
-                    Diagnostic.cluster_id,
-                    SESpecific.cluster_id,
-                ],
-                OUTPUT_CLUSTERS: [
-                    Identify.cluster_id,
-                    Groups.cluster_id,
-                    Scenes.cluster_id,
-                    OnOff.cluster_id,
-                    LevelControl.cluster_id,
-                    WindowCovering.cluster_id,
-                ],
-            },
-            242: {
-                PROFILE_ID: zgp.PROFILE_ID,
-                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
-                INPUT_CLUSTERS: [],
-                OUTPUT_CLUSTERS: [
-                    GreenPowerProxy.cluster_id,
-                ],
-            },
-        },
-    }
-    replacement = {
-        ENDPOINTS: {
-            3: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT,
-                INPUT_CLUSTERS: [
-                    SEBasic,
-                    Identify.cluster_id,
-                    Groups.cluster_id,
-                    Scenes.cluster_id,
-                    OnOff.cluster_id,
-                    LevelControl.cluster_id,
-                    SEBallast,
-                    Diagnostic.cluster_id,
-                ],
-                OUTPUT_CLUSTERS: [
-                    Ota.cluster_id,
-                ],
-            },
-            21: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.DIMMER_SWITCH,
-                INPUT_CLUSTERS: [
-                    SEBasic,
-                    Identify.cluster_id,
-                    Diagnostic.cluster_id,
-                    SESpecific,
-                ],
-                OUTPUT_CLUSTERS: [
-                    Identify.cluster_id,
-                    Groups.cluster_id,
-                    Scenes.cluster_id,
-                    OnOff.cluster_id,
-                    LevelControl.cluster_id,
-                    WindowCovering.cluster_id,
-                ],
-            },
-            242: {
-                PROFILE_ID: zgp.PROFILE_ID,
-                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
-                INPUT_CLUSTERS: [],
-                OUTPUT_CLUSTERS: [
-                    GreenPowerProxy.cluster_id,
-                ],
-            },
-        }
-    }
+(
+    QuirkBuilder(SE_MANUF_NAME, "NHPB/SWITCH/1")
+    .replaces(SEBasic)
+    .replaces(SEOnOff)
+    .replaces(SEBasic, endpoint_id=21)
+    .replaces(SESpecific, endpoint_id=21)
+    .add_to_registry()
+)
diff --git a/zhaquirks/sinope/__init__.py b/zhaquirks/sinope/__init__.py
index ff59f31..7990b36 100644
--- a/zhaquirks/sinope/__init__.py
+++ b/zhaquirks/sinope/__init__.py
@@ -52,7 +52,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_ON,
-            VALUE: ButtonAction.Pressed_on,
+            VALUE: int(ButtonAction.Pressed_on),
         },
     },
     (SHORT_PRESS, TURN_OFF): {
@@ -63,7 +63,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_OFF,
-            VALUE: ButtonAction.Pressed_off,
+            VALUE: int(ButtonAction.Pressed_off),
         },
     },
     (SHORT_RELEASE, TURN_ON): {
@@ -74,7 +74,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_ON,
-            VALUE: ButtonAction.Released_on,
+            VALUE: int(ButtonAction.Released_on),
         },
     },
     (SHORT_RELEASE, TURN_OFF): {
@@ -85,7 +85,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_OFF,
-            VALUE: ButtonAction.Released_off,
+            VALUE: int(ButtonAction.Released_off),
         },
     },
     (DOUBLE_PRESS, TURN_ON): {
@@ -96,7 +96,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_ON,
-            VALUE: ButtonAction.Double_on,
+            VALUE: int(ButtonAction.Double_on),
         },
     },
     (DOUBLE_PRESS, TURN_OFF): {
@@ -107,7 +107,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_OFF,
-            VALUE: ButtonAction.Double_off,
+            VALUE: int(ButtonAction.Double_off),
         },
     },
     (LONG_PRESS, TURN_ON): {
@@ -118,7 +118,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_ON,
-            VALUE: ButtonAction.Long_on,
+            VALUE: int(ButtonAction.Long_on),
         },
     },
     (LONG_PRESS, TURN_OFF): {
@@ -129,7 +129,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_OFF,
-            VALUE: ButtonAction.Long_off,
+            VALUE: int(ButtonAction.Long_off),
         },
     },
 }
diff --git a/zhaquirks/tuya/air/ts0601_air_quality.py b/zhaquirks/tuya/air/ts0601_air_quality.py
index 8a56667..ff528c1 100644
--- a/zhaquirks/tuya/air/ts0601_air_quality.py
+++ b/zhaquirks/tuya/air/ts0601_air_quality.py
@@ -37,6 +37,7 @@ class TuyaCO2Sensor(CustomDevice):
             ("_TZE200_yvx5lh6k", "TS0601"),
             ("_TZE204_yvx5lh6k", "TS0601"),
             ("_TZE200_c2fmom5z", "TS0601"),
+            ("_TZE204_c2fmom5z", "TS0601"),
         ],
         ENDPOINTS: {
             1: {
@@ -89,6 +90,7 @@ class TuyaCO2SensorGPP(CustomDevice):
             ("_TZE200_yvx5lh6k", "TS0601"),
             ("_TZE204_yvx5lh6k", "TS0601"),
             ("_TZE200_c2fmom5z", "TS0601"),
+            ("_TZE204_c2fmom5z", "TS0601"),
         ],
         ENDPOINTS: {
             1: {
@@ -152,6 +154,7 @@ class TuyaNDIRCO2SensorGPP(CustomDevice):
         # output_clusters=[25, 10])
         MODELS_INFO: [
             ("_TZE200_ogkdpgy2", "TS0601"),
+            ("_TZE204_ogkdpgy2", "TS0601"),
         ],
         ENDPOINTS: {
             1: {
diff --git a/zhaquirks/tuya/ts0601_illuminance.py b/zhaquirks/tuya/ts0601_illuminance.py
index 32505d2..0a298bf 100644
--- a/zhaquirks/tuya/ts0601_illuminance.py
+++ b/zhaquirks/tuya/ts0601_illuminance.py
@@ -72,6 +72,7 @@ class TuyaIlluminance(CustomDevice):
         #  input_clusters=[4, 5, 61184, 0], output_clusters=[25, 10])
         MODELS_INFO: [
             ("_TZE200_khx7nnka", "TS0601"),
+            ("_TZE204_khx7nnka", "TS0601"),
             ("_TZE200_yi4jtqq1", "TS0601"),
         ],
         ENDPOINTS: {
diff --git a/zhaquirks/tuya/ts0601_sensor.py b/zhaquirks/tuya/ts0601_sensor.py
index b9c538d..4910bd8 100644
--- a/zhaquirks/tuya/ts0601_sensor.py
+++ b/zhaquirks/tuya/ts0601_sensor.py
@@ -1,5 +1,10 @@
 """Tuya temp and humidity sensors."""
 
+from zigpy.quirks.v2.homeassistant import EntityType
+from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass
+from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass
+import zigpy.types as t
+
 from zhaquirks.tuya.builder import TuyaPowerConfigurationCluster2AAA, TuyaQuirkBuilder
 
 (
@@ -29,6 +34,7 @@ from zhaquirks.tuya.builder import TuyaPowerConfigurationCluster2AAA, TuyaQuirkB
 (
     TuyaQuirkBuilder("_TZE200_yjjdcqsq", "TS0601")
     .applies_to("_TZE200_9yapgbuv", "TS0601")
+    .applies_to("_TZE204_9yapgbuv", "TS0601")
     .applies_to("_TZE204_yjjdcqsq", "TS0601")
     .applies_to("_TZE200_utkemkbs", "TS0601")
     .applies_to("_TZE204_utkemkbs", "TS0601")
@@ -71,3 +77,26 @@ from zhaquirks.tuya.builder import TuyaPowerConfigurationCluster2AAA, TuyaQuirkB
     .skip_configuration()
     .add_to_registry()
 )
+
+(
+    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_binary_sensor(
+        dp_id=1,
+        attribute_name="zone_status",
+        device_class=BinarySensorDeviceClass.OPENING,
+        fallback_name="Opening",
+        entity_type=EntityType.STANDARD,
+    )
+    .tuya_battery(dp_id=2)
+    .skip_configuration()
+    .add_to_registry()
+)


Test patch:
diff --git a/tests/test_schneiderelectric.py b/tests/test_schneiderelectric.py
index 6695114..0bf8551 100644
--- a/tests/test_schneiderelectric.py
+++ b/tests/test_schneiderelectric.py
@@ -6,7 +6,6 @@ from zigpy.zcl import foundation
 from zigpy.zcl.clusters.closures import WindowCovering
 
 from tests.common import ClusterListener
-import zhaquirks.schneiderelectric.dimmers
 import zhaquirks.schneiderelectric.shutters
 
 zhaquirks.setup()
@@ -136,62 +135,3 @@ async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_quirk):
         23,  # 100 - 77
     )
     assert len(cluster_listener.cluster_commands) == 0
-
-
-def test_nh_rotary_dimmer_1_signature(assert_signature_matches_quirk):
-    """Test signature."""
-    signature = {
-        "node_descriptor": (
-            "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, "
-            "user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, "
-            "mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered|RxOnWhenIdle|"
-            "AllocateAddress: 142>, manufacturer_code=4190, maximum_buffer_size=82, maximum_incoming_transfer_size=82, "
-            "server_mask=11264, maximum_outgoing_transfer_size=82, "
-            "descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, "
-            "*is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, "
-            "*is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, "
-            "*is_security_capable=False)"
-        ),
-        "endpoints": {
-            "3": {
-                "profile_id": 0x0104,
-                "device_type": "0x0101",
-                "in_clusters": [
-                    "0x0000",
-                    "0x0003",
-                    "0x0004",
-                    "0x0005",
-                    "0x0006",
-                    "0x0008",
-                    "0x0301",
-                    "0x0b05",
-                ],
-                "out_clusters": ["0x0019"],
-            },
-            "21": {
-                "profile_id": 0x0104,
-                "device_type": "0x0104",
-                "in_clusters": ["0x0000", "0x0003", "0x0b05", "0xff17"],
-                "out_clusters": [
-                    "0x0003",
-                    "0x0004",
-                    "0x0005",
-                    "0x0006",
-                    "0x0008",
-                    "0x0102",
-                ],
-            },
-            "242": {
-                "profile_id": 0xA1E0,
-                "device_type": "0x0061",
-                "in_clusters": [],
-                "out_clusters": ["0x0021"],
-            },
-        },
-        "manufacturer": "Schneider Electric",
-        "model": "NHROTARY/DIMMER/1",
-        "class": "zigpy.device.Device",
-    }
-    assert_signature_matches_quirk(
-        zhaquirks.schneiderelectric.dimmers.NHRotaryDimmer1, signature
-    )
diff --git a/tests/test_sinope.py b/tests/test_sinope.py
index 3f377a8..72b3e69 100644
--- a/tests/test_sinope.py
+++ b/tests/test_sinope.py
@@ -239,3 +239,18 @@ async def test_sinope_light_switch_reporting(zigpy_device_from_quirk, quirk):
 
         assert len(request_mock.mock_calls) == 1
         assert len(bind_mock.mock_calls) == 1
+
+
+@pytest.mark.parametrize("quirk", (SinopeTechnologieslight,))
+async def test_sinope_light_device_triggers_def(zigpy_device_from_quirk, quirk):
+    """Test device automation triggers.
+
+    Make sure that values are actual ints and not instances of an enum class.
+    """
+
+    device: Device = zigpy_device_from_quirk(quirk)
+
+    for config in device.device_automation_triggers.values():
+        val = config.get("args", {}).get("value")
+        if val is not None:
+            assert type(val) is int, type(val)