Repository: zigpy/zha-device-handlers
Pull request #2674 (2023-10-21 19:20:03)

Add quirk IDs to Tuya plugs
## Proposed change
This adds quirk IDs which are later used in another Home Assistant PR to let ZHA match against these `quirk_ids`.


## Additional information
- Reference for base ZHA quirks ID PR: https://github.com/home-assistant/core/pull/102482

This PR can already be merged independently.

- These quirk IDs will be used by: https://github.com/home-assistant/core/pull/102489

## Checklist
- [x] The changes are tested and work correctly (with linked PRs above)
- [x] `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/2674?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:
> Comparison is base [(`6ff75e7`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/6ff75e7aa2b6e8bfc036b8114f9b37b3fa795e6a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 86.80% compared to head [(`40e9e33`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2674?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 86.86%.

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


```diff
@@            Coverage Diff             @@
##              dev    #2674      +/-   ##
==========================================
+ Coverage   86.80%   86.86%   +0.06%     
==========================================
  Files         280      281       +1     
  Lines        8571     8612      +41     
==========================================
+ Hits         7440     7481      +41     
  Misses       1131     1131              
```


| [Files](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2674?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) | Coverage Δ | |
|---|---|---|
| [zhaquirks/quirk\_ids.py](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2674?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3F1aXJrX2lkcy5weQ==) | `100.00% <100.00%> (ø)` | |
| [zhaquirks/tuya/ts000x.py](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2674?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3R1eWEvdHMwMDB4LnB5) | `100.00% <100.00%> (ø)` | |
| [zhaquirks/tuya/ts011f\_plug.py](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2674?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3R1eWEvdHMwMTFmX3BsdWcucHk=) | `100.00% <100.00%> (ø)` | |
| [zhaquirks/tuya/ts0121\_plug.py](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2674?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3R1eWEvdHMwMTIxX3BsdWcucHk=) | `100.00% <100.00%> (ø)` | |
| [zhaquirks/tuya/ts0601\_switch.py](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2674?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3R1eWEvdHMwNjAxX3N3aXRjaC5weQ==) | `100.00% <100.00%> (ø)` | |


</details>

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


Patch:
diff --git a/zhaquirks/quirk_ids.py b/zhaquirks/quirk_ids.py
new file mode 100644
index 0000000..8306c91
--- /dev/null
+++ b/zhaquirks/quirk_ids.py
@@ -0,0 +1,5 @@
+"""Quirk IDs used for matching quirked devices in ZHA."""
+
+# Tuya
+TUYA_PLUG_ONOFF = "tuya.plug_on_off_attributes"  # plugs with configurable attributes on the OnOff cluster
+TUYA_PLUG_MANUFACTURER = "tuya.plug_manufacturer_attributes"  # plugs with configurable attributes on a custom cluster
diff --git a/zhaquirks/tuya/ts000x.py b/zhaquirks/tuya/ts000x.py
index f807cad..7875a71 100644
--- a/zhaquirks/tuya/ts000x.py
+++ b/zhaquirks/tuya/ts000x.py
@@ -20,6 +20,7 @@ from zhaquirks.const import (
     OUTPUT_CLUSTERS,
     PROFILE_ID,
 )
+from zhaquirks.quirk_ids import TUYA_PLUG_ONOFF
 from zhaquirks.tuya import (
     TuyaZBE000Cluster,
     TuyaZBElectricalMeasurement,
@@ -33,6 +34,8 @@ from zhaquirks.tuya.mcu import EnchantedDevice
 class Switch_1G_GPP(EnchantedDevice):
     """Tuya 1 gang switch module with restore power state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0001",
         ENDPOINTS: {
@@ -89,6 +92,8 @@ class Switch_1G_GPP(EnchantedDevice):
 class Switch_1G_Metering(EnchantedDevice):
     """Tuya 1 gang switch with metering support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0001",
         ENDPOINTS: {
@@ -148,6 +153,8 @@ class Switch_1G_Metering(EnchantedDevice):
 class Switch_2G_GPP(EnchantedDevice):
     """Tuya 2 gang switch module with restore power state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0002",
         ENDPOINTS: {
@@ -236,6 +243,8 @@ class Switch_2G_GPP(EnchantedDevice):
 class Switch_2G_Metering(EnchantedDevice):
     """Tuya 2 gang switch with metering support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0002",
         ENDPOINTS: {
@@ -324,6 +333,8 @@ class Switch_2G_Metering(EnchantedDevice):
 class Switch_2G_Var03(EnchantedDevice):
     """Tuya 2 gang (variation 03)."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0002",
         ENDPOINTS: {
@@ -416,6 +427,8 @@ class Switch_2G_Var03(EnchantedDevice):
 class Switch_3G_GPP(EnchantedDevice):
     """Tuya 3 gang switch module with restore power state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0003",
         ENDPOINTS: {
@@ -530,6 +543,8 @@ class Switch_3G_GPP(EnchantedDevice):
 class Switch_3G_Metering(EnchantedDevice):
     """Tuya 3 gang switch with metering support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0003",
         ENDPOINTS: {
@@ -641,6 +656,8 @@ class Switch_3G_Metering(EnchantedDevice):
 class Switch_3G_GPP_Var2(EnchantedDevice):
     """Tuya 3 gang switch module."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0003",
         ENDPOINTS: {
@@ -754,6 +771,8 @@ class Switch_3G_GPP_Var2(EnchantedDevice):
 class Switch_4G_GPP(EnchantedDevice):
     """Tuya 4 gang switch module with restore power state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0004",
         ENDPOINTS: {
@@ -894,6 +913,8 @@ class Switch_4G_GPP(EnchantedDevice):
 class Switch_4G_Metering(EnchantedDevice):
     """Tuya 4 gang switch with metering support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0004",
         ENDPOINTS: {
@@ -1028,6 +1049,8 @@ class Switch_4G_Metering(EnchantedDevice):
 class Switch_4G_GPP_Var2(EnchantedDevice):
     """Tuya 4 gang switch module."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0004",
         ENDPOINTS: {
diff --git a/zhaquirks/tuya/ts011f_plug.py b/zhaquirks/tuya/ts011f_plug.py
index 1f540e1..2a053c8 100644
--- a/zhaquirks/tuya/ts011f_plug.py
+++ b/zhaquirks/tuya/ts011f_plug.py
@@ -26,6 +26,7 @@ from zhaquirks.const import (
     OUTPUT_CLUSTERS,
     PROFILE_ID,
 )
+from zhaquirks.quirk_ids import TUYA_PLUG_ONOFF
 from zhaquirks.tuya import (
     TuyaNewManufCluster,
     TuyaZB1888Cluster,
@@ -42,6 +43,8 @@ from zhaquirks.tuya.mcu import EnchantedDevice
 class Plug(EnchantedDevice):
     """Tuya plug with restore power state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -102,6 +105,8 @@ class Plug(EnchantedDevice):
 class Plug_1AC(CustomDevice):
     """Tuya plug without metering with restore power state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -160,6 +165,8 @@ class Plug_1AC(CustomDevice):
 class Plug_2AC_2USB(EnchantedDevice):
     """Tuya 2 outlet + 2 USB with restore power state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODELS_INFO: [("_TZ3000_3zofvcaa", "TS011F")],
         ENDPOINTS: {
@@ -297,6 +304,8 @@ class Plug_2AC_2USB(EnchantedDevice):
 class Plug_3AC_4USB(CustomDevice):
     """Tuya 3 outlet + 4 USB with restore power state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -407,6 +416,8 @@ class Plug_3AC_4USB(CustomDevice):
 class Plug_4AC_2USB(CustomDevice):
     """Tuya 4 outlet + 2 USB surge protector with restore power state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -589,6 +600,8 @@ class Plug_4AC_2USB(CustomDevice):
 class Plug_TZ3210_2AC(CustomDevice):
     """TS0011F 2 outlet plug."""
 
+    # quirk_id = TUYA_PLUG_ONOFF  # TODO: Why does this quirk not have OnOffAttributeCluster??
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -682,6 +695,8 @@ class Plug_TZ3210_2AC(CustomDevice):
 class Plug_TZ3210_1AC(CustomDevice):
     """TS0011F 1 outlet plug."""
 
+    # quirk_id = TUYA_PLUG_ONOFF  # TODO: Why does this quirk not have OnOffAttributeCluster??
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -747,6 +762,8 @@ class Plug_TZ3210_1AC(CustomDevice):
 class Plug_4AC_2USB_cfnprab5(EnchantedDevice):
     """Tuya 4 outlet + 2 USB surge protector with restore power state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -913,6 +930,8 @@ class Plug_4AC_2USB_cfnprab5(EnchantedDevice):
 class Plug_4AC_2USB_Metering(EnchantedDevice):
     """Tuya Tuya 4 outlet + 2 USB with power metering."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -1031,6 +1050,8 @@ class Plug_4AC_2USB_Metering(EnchantedDevice):
 class Plug_v2(EnchantedDevice):
     """Another TS011F Tuya plug. First one using this definition is _TZ3000_okaz9tjs."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -1079,6 +1100,8 @@ class Plug_v2(EnchantedDevice):
 class Plug_v3(EnchantedDevice):
     """Tuya TS011F plug. One plug is _Tz3000_0Zfrhq4I."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -1150,6 +1173,8 @@ class Plug_v3(EnchantedDevice):
 class Plug_2AC_var03(CustomDevice):
     """Tuya 2 socket wall outlet with child lock and power-restore state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -1220,6 +1245,8 @@ class Plug_2AC_var03(CustomDevice):
 class Plug_CB_Metering(EnchantedDevice):
     """Circuit breaker with monitoring, e.g. Tongou TO-Q-SY1-JZT. First one using this definition was _TZ3000_qeuvnohg."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         MODELS_INFO: [("_TZ3000_qeuvnohg", "TS011F")],
@@ -1288,6 +1315,8 @@ class Plug_CB_Metering(EnchantedDevice):
 class Plug_2AC_var05(EnchantedDevice):
     """Immax TS0011F 2 outlet plug."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
diff --git a/zhaquirks/tuya/ts0121_plug.py b/zhaquirks/tuya/ts0121_plug.py
index 286fe57..0df931a 100644
--- a/zhaquirks/tuya/ts0121_plug.py
+++ b/zhaquirks/tuya/ts0121_plug.py
@@ -22,6 +22,7 @@ from zhaquirks.const import (
     OUTPUT_CLUSTERS,
     PROFILE_ID,
 )
+from zhaquirks.quirk_ids import TUYA_PLUG_ONOFF
 from zhaquirks.tuya import (
     TuyaNewManufCluster,
     TuyaZBE000Cluster,
@@ -35,6 +36,8 @@ from zhaquirks.tuya import (
 class Plug(CustomDevice):
     """Tuya TS0121 plug with restore tuya power state support."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0121",
         ENDPOINTS: {
@@ -79,6 +82,8 @@ class Plug(CustomDevice):
 class TS0121B(CustomDevice):
     """Tuya TS0121 plug with restore tuya power state support and ZGP endpoint."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0121",
         ENDPOINTS: {
@@ -137,6 +142,8 @@ class TS0121B(CustomDevice):
 class TS0121_Var03(CustomDevice):
     """Tuya TS0121 plug DeviceType.MAIN_POWER_OUTLET."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS0121",
         ENDPOINTS: {
diff --git a/zhaquirks/tuya/ts0601_sensor.py b/zhaquirks/tuya/ts0601_sensor.py
index 41114b8..aaed2ba 100644
--- a/zhaquirks/tuya/ts0601_sensor.py
+++ b/zhaquirks/tuya/ts0601_sensor.py
@@ -75,6 +75,26 @@ class TemperatureHumidityManufCluster(TuyaMCUCluster):
     }
 
 
+class TemperatureHumidityBatteryStatesManufCluster(TuyaMCUCluster):
+    """Tuya Manufacturer Cluster with Temperature and Humidity data points. Battery states 25, 50 and 100%."""
+
+    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
+        1: TemperatureHumidityManufCluster.dp_to_attribute[1],
+        2: TemperatureHumidityManufCluster.dp_to_attribute[2],
+        3: DPToAttributeMapping(
+            TuyaPowerConfigurationCluster2AAA.ep_attribute,
+            "battery_percentage_remaining",
+            converter=lambda x: {0: 25, 1: 50, 2: 100}[x],  # double reported percentage
+        ),
+    }
+
+    data_point_handlers = {
+        1: "_dp_2_attr_update",
+        2: "_dp_2_attr_update",
+        3: "_dp_2_attr_update",
+    }
+
+
 class TuyaTempHumiditySensor(CustomDevice):
     """Custom device representing tuya temp and humidity sensor with e-ink screen."""
 
@@ -181,8 +201,6 @@ class TuyaTempHumiditySensorVar03(CustomDevice):
         # "in_clusters": ["0x0000","0x0004","0x0005","0xef00"],
         # "out_clusters": ["0x000a","0x0019"]
         MODELS_INFO: [
-            ("_TZE200_yjjdcqsq", "TS0601"),
-            ("_TZE200_9yapgbuv", "TS0601"),
             ("_TZE200_qyflbnbj", "TS0601"),
             ("_TZE200_utkemkbs", "TS0601"),
         ],
@@ -221,6 +239,53 @@ class TuyaTempHumiditySensorVar03(CustomDevice):
     }
 
 
+class TuyaTempHumiditySensorVar04(CustomDevice):
+    """Tuya temp and humidity sensor (variation 04)."""
+
+    signature = {
+        # "profile_id": 260,
+        # "device_type": "0x0051",
+        # "in_clusters": ["0x0000","0x0004","0x0005","0xef00"],
+        # "out_clusters": ["0x000a","0x0019"]
+        MODELS_INFO: [
+            ("_TZE200_yjjdcqsq", "TS0601"),
+            ("_TZE200_9yapgbuv", "TS0601"),
+        ],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TemperatureHumidityManufCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id],
+            }
+        },
+    }
+
+    replacement = {
+        SKIP_CONFIGURATION: True,
+        ENDPOINTS: {
+            1: {
+                DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TemperatureHumidityBatteryStatesManufCluster,
+                    TuyaTemperatureMeasurement,
+                    TuyaRelativeHumidity,
+                    TuyaPowerConfigurationCluster2AAA,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id],
+            }
+        },
+    }
+
+
 class SoilManufCluster(TuyaMCUCluster):
     """Tuya Manufacturer Cluster with Temperature and Humidity data points."""
 
diff --git a/zhaquirks/tuya/ts0601_switch.py b/zhaquirks/tuya/ts0601_switch.py
index da2c34f..c23e3fa 100644
--- a/zhaquirks/tuya/ts0601_switch.py
+++ b/zhaquirks/tuya/ts0601_switch.py
@@ -10,6 +10,7 @@ from zhaquirks.const import (
     OUTPUT_CLUSTERS,
     PROFILE_ID,
 )
+from zhaquirks.quirk_ids import TUYA_PLUG_MANUFACTURER
 from zhaquirks.tuya import TuyaSwitch
 from zhaquirks.tuya.mcu import (
     MoesSwitchManufCluster,
@@ -22,6 +23,8 @@ from zhaquirks.tuya.mcu import (
 class TuyaSingleSwitchTI(TuyaSwitch):
     """Tuya single channel switch time on in cluster device."""
 
+    quirk_id = TUYA_PLUG_MANUFACTURER
+
     signature = {
         # "node_descriptor": "<NodeDescriptor byte1=1 byte2=64 mac_capability_flags=142 manufacturer_code=4098
         #                       maximum_buffer_size=82 maximum_incoming_transfer_size=82 server_mask=11264
@@ -68,6 +71,8 @@ class TuyaSingleSwitchTI(TuyaSwitch):
 class TuyaSingleSwitchTO(TuyaSwitch):
     """Tuya single channel switch time on out cluster device."""
 
+    quirk_id = TUYA_PLUG_MANUFACTURER
+
     signature = {
         # "node_descriptor": "<NodeDescriptor byte1=1 byte2=64 mac_capability_flags=142 manufacturer_code=4098
         #                       maximum_buffer_size=82 maximum_incoming_transfer_size=82 server_mask=11264
@@ -176,6 +181,8 @@ class TuyaSingleSwitch_GP(TuyaSwitch):
 class TuyaDoubleSwitchTO(TuyaSwitch):
     """Tuya double channel switch time on out cluster device."""
 
+    quirk_id = TUYA_PLUG_MANUFACTURER
+
     signature = {
         # "node_descriptor": "<NodeDescriptor byte1=1 byte2=64 mac_capability_flags=142 manufacturer_code=4098
         #                       maximum_buffer_size=82 maximum_incoming_transfer_size=82 server_mask=11264
@@ -231,6 +238,8 @@ class TuyaDoubleSwitchTO(TuyaSwitch):
 class TuyaDoubleSwitch_GP(TuyaSwitch):
     """Tuya double channel switch with GreenPowerProxy cluster device."""
 
+    quirk_id = TUYA_PLUG_MANUFACTURER
+
     signature = {
         MODELS_INFO: [
             ("_TZE200_7deq70b8", "TS0601"),
@@ -297,6 +306,8 @@ class TuyaDoubleSwitch_GP(TuyaSwitch):
 class TuyaTripleSwitchTO(TuyaSwitch):
     """Tuya triple channel switch time on out cluster device."""
 
+    quirk_id = TUYA_PLUG_MANUFACTURER
+
     signature = {
         MODELS_INFO: [
             # ("_TZE200_kyfqmmyl", "TS0601"),  ## candidate reported in #716
@@ -353,6 +364,8 @@ class TuyaTripleSwitchTO(TuyaSwitch):
 class TuyaTripleSwitch_GP(TuyaSwitch):
     """Tuya triple channel switch with GreenPowerProxy cluster device."""
 
+    quirk_id = TUYA_PLUG_MANUFACTURER
+
     signature = {
         MODELS_INFO: [
             ("_TZE200_2hf7x9n3", "TS0601"),  # reported #1597
@@ -427,6 +440,8 @@ class TuyaTripleSwitch_GP(TuyaSwitch):
 class TuyaQuadrupleSwitchTO(TuyaSwitch):
     """Tuya quadruple channel switch time on out cluster device."""
 
+    quirk_id = TUYA_PLUG_MANUFACTURER
+
     signature = {
         MODELS_INFO: [
             ("_TZE200_aqnazj70", "TS0601"),
@@ -574,6 +589,8 @@ class TuyaQuadrupleSwitch_GP(TuyaSwitch):
 class TuyaSextupleSwitchTO(TuyaSwitch):
     """Tuya sextuple channel switch time on out cluster device."""
 
+    quirk_id = TUYA_PLUG_MANUFACTURER
+
     signature = {
         # <SimpleDescriptor endpoint=1 profile=260 device_type=81
         # input_clusters=[0x0000,0x0004,0x0005,0xEF00]
@@ -656,6 +673,8 @@ class TuyaSextupleSwitchTO(TuyaSwitch):
 class TuyaSextupleSwitchTO_GP(TuyaSwitch):
     """Tuya sextuple channel switch time on out cluster device with GreenPowerProxy cluster device."""
 
+    quirk_id = TUYA_PLUG_MANUFACTURER
+
     signature = {
         MODELS_INFO: [
             ("_TZE200_9mahtqtg", "TS0601"),
diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py
index e6a8a24..b989354 100644
--- a/zhaquirks/xiaomi/__init__.py
+++ b/zhaquirks/xiaomi/__init__.py
@@ -18,7 +18,6 @@ from zigpy.zcl.clusters.general import (
     PowerConfiguration,
 )
 from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
-from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster
 from zigpy.zcl.clusters.measurement import (
     IlluminanceMeasurement,
     PressureMeasurement,
@@ -443,10 +442,11 @@ class BasicCluster(XiaomiCluster, Basic):
     """Xiaomi basic cluster implementation."""
 
 
-class XiaomiAqaraE1Cluster(XiaomiCluster, ManufacturerSpecificCluster):
+class XiaomiAqaraE1Cluster(XiaomiCluster):
     """Xiaomi mfg cluster implementation."""
 
     cluster_id = 0xFCC0
+    ep_attribute = "opple_cluster"
 
 
 class BinaryOutputInterlock(CustomCluster, BinaryOutput):
diff --git a/zhaquirks/xiaomi/aqara/feeder_acn001.py b/zhaquirks/xiaomi/aqara/feeder_acn001.py
index 54ec170..35e9865 100644
--- a/zhaquirks/xiaomi/aqara/feeder_acn001.py
+++ b/zhaquirks/xiaomi/aqara/feeder_acn001.py
@@ -96,7 +96,6 @@ class OppleCluster(XiaomiAqaraE1Cluster):
         Manual = 0x00
         Schedule = 0x01
 
-    ep_attribute: str = "opple_cluster"
     attributes = {
         ZCL_FEEDING: ("feeding", types.Bool, True),
         ZCL_LAST_FEEDING_SOURCE: ("last_feeding_source", FeedingSource, True),
diff --git a/zhaquirks/xiaomi/aqara/illumination.py b/zhaquirks/xiaomi/aqara/illumination.py
index 37e5868..28f96bb 100644
--- a/zhaquirks/xiaomi/aqara/illumination.py
+++ b/zhaquirks/xiaomi/aqara/illumination.py
@@ -74,7 +74,6 @@ class Illumination(XiaomiCustomDevice):
 class OppleCluster(XiaomiAqaraE1Cluster):
     """Opple cluster with configurable detection interval."""
 
-    ep_attribute = "opple_cluster"
     attributes = {
         0x0000: ("detection_interval", types.uint16_t, True),
     }
diff --git a/zhaquirks/xiaomi/aqara/light_acn.py b/zhaquirks/xiaomi/aqara/light_acn.py
new file mode 100644
index 0000000..b9c50f1
--- /dev/null
+++ b/zhaquirks/xiaomi/aqara/light_acn.py
@@ -0,0 +1,86 @@
+from zigpy import types as t
+from zigpy.profiles import zha
+from zigpy.zcl.clusters.general import (
+    Groups,
+    Identify,
+    LevelControl,
+    OnOff,
+    Ota,
+    Scenes,
+    Time,
+)
+from zigpy.zcl.clusters.lighting import Color
+
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+from zhaquirks.xiaomi import BasicCluster, XiaomiAqaraE1Cluster, XiaomiCustomDevice
+
+
+class OppleClusterLight(XiaomiAqaraE1Cluster):
+    """Add Opple cluster for power outage memory attribute."""
+
+    attributes = {
+        0x0201: ("power_outage_memory", t.Bool, True),
+    }
+
+
+class LumiLightAcn003(XiaomiCustomDevice):
+    """Quirk for Aqara ceiling light L1-350 also known as Xiaomi ZNXDD01LM.
+
+    Provides dimmable light control with color temperature setting.
+    This quirk adds support for power on behavior by adding the power_outage_memory attribute.
+    """
+
+    signature = {
+        MODELS_INFO: [
+            ("Aqara", "lumi.light.acn003"),
+        ],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
+                INPUT_CLUSTERS: [
+                    BasicCluster.cluster_id,  # 0x0000
+                    Identify.cluster_id,  # 0x0003
+                    Groups.cluster_id,  # 0x0004
+                    Scenes.cluster_id,  # 0x0005
+                    OnOff.cluster_id,  # 0x0006
+                    LevelControl.cluster_id,  # 0x0008
+                    Color.cluster_id,  # 0x0300
+                    OppleClusterLight.cluster_id,  # 0xFCC0 - manufacturer specific
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,  # 0x000A
+                    Ota.cluster_id,  # 0x0019
+                ],
+            }
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
+                INPUT_CLUSTERS: [
+                    BasicCluster,
+                    Identify.cluster_id,  # 0x0003
+                    Groups.cluster_id,  # 0x0004
+                    Scenes.cluster_id,  # 0x0005
+                    OnOff.cluster_id,  # 0x0006
+                    LevelControl.cluster_id,  # 0x0008
+                    Color.cluster_id,  # 0x0300
+                    OppleClusterLight,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,  # 0x000A
+                    Ota.cluster_id,  # 0x0019
+                ],
+            }
+        }
+    }
diff --git a/zhaquirks/xiaomi/aqara/motion_ac01.py b/zhaquirks/xiaomi/aqara/motion_ac01.py
index 46250c6..c3fc3ad 100644
--- a/zhaquirks/xiaomi/aqara/motion_ac01.py
+++ b/zhaquirks/xiaomi/aqara/motion_ac01.py
@@ -53,7 +53,6 @@ class AqaraPresenceEvents(types.enum8):
 class OppleCluster(XiaomiAqaraE1Cluster):
     """Opple cluster."""
 
-    ep_attribute = "opple_cluster"
     attributes = {
         PRESENCE: ("presence", types.uint8_t, True),
         MONITORING_MODE: ("monitoring_mode", types.uint8_t, True),
diff --git a/zhaquirks/xiaomi/aqara/motion_ac02.py b/zhaquirks/xiaomi/aqara/motion_ac02.py
index 0ec276a..a285f2e 100644
--- a/zhaquirks/xiaomi/aqara/motion_ac02.py
+++ b/zhaquirks/xiaomi/aqara/motion_ac02.py
@@ -37,7 +37,6 @@ _LOGGER = logging.getLogger(__name__)
 class OppleCluster(XiaomiAqaraE1Cluster):
     """Opple cluster."""
 
-    ep_attribute = "opple_cluster"
     attributes = {
         DETECTION_INTERVAL: ("detection_interval", types.uint8_t, True),
         MOTION_SENSITIVITY: ("motion_sensitivity", types.uint8_t, True),
diff --git a/zhaquirks/xiaomi/aqara/motion_agl04.py b/zhaquirks/xiaomi/aqara/motion_agl04.py
index 19c5737..789462f 100644
--- a/zhaquirks/xiaomi/aqara/motion_agl04.py
+++ b/zhaquirks/xiaomi/aqara/motion_agl04.py
@@ -33,7 +33,6 @@ MOTION_SENSITIVITY = 0x010C
 class OppleCluster(XiaomiAqaraE1Cluster):
     """Opple cluster."""
 
-    ep_attribute = "opple_cluster"
     attributes = {
         DETECTION_INTERVAL: ("detection_interval", types.uint8_t, True),
         MOTION_SENSITIVITY: ("motion_sensitivity", types.uint8_t, True),
diff --git a/zhaquirks/xiaomi/aqara/plug_eu.py b/zhaquirks/xiaomi/aqara/plug_eu.py
index cf1fd84..f1d30bf 100644
--- a/zhaquirks/xiaomi/aqara/plug_eu.py
+++ b/zhaquirks/xiaomi/aqara/plug_eu.py
@@ -60,7 +60,6 @@ async def remove_from_ep(dev: zigpy.device.Device) -> None:
 class OppleCluster(XiaomiAqaraE1Cluster):
     """Opple cluster."""
 
-    ep_attribute = "opple_cluster"
     attributes = {
         0x0009: ("mode", types.uint8_t, True),
         0x0201: ("power_outage_memory", types.Bool, True),
diff --git a/zhaquirks/xiaomi/aqara/remote_h1.py b/zhaquirks/xiaomi/aqara/remote_h1.py
index bd668f3..d2500cc 100644
--- a/zhaquirks/xiaomi/aqara/remote_h1.py
+++ b/zhaquirks/xiaomi/aqara/remote_h1.py
@@ -54,8 +54,6 @@ BOTH_BUTTONS = "both_buttons"
 class AqaraRemoteManuSpecificCluster(XiaomiAqaraE1Cluster):
     """Aqara manufacturer specific settings."""
 
-    ep_attribute = "aqara_cluster"
-
     # manufacture override code: 4447 (0x115f)
     # to get/set these attributes, you might need to click the button 5 times
     # quickly.
diff --git a/zhaquirks/xiaomi/aqara/smoke.py b/zhaquirks/xiaomi/aqara/smoke.py
index c668897..ede4671 100644
--- a/zhaquirks/xiaomi/aqara/smoke.py
+++ b/zhaquirks/xiaomi/aqara/smoke.py
@@ -50,7 +50,6 @@ SMOKE_DENSITY_DBM_MAP = {
 class OppleCluster(XiaomiAqaraE1Cluster):
     """Opple cluster."""
 
-    ep_attribute = "opple_cluster"
     attributes = {
         BUZZER_MANUAL_MUTE: ("buzzer_manual_mute", types.uint8_t, True),
         SELF_TEST: ("self_test", types.Bool, True),
diff --git a/zhaquirks/xiaomi/aqara/switch_t1.py b/zhaquirks/xiaomi/aqara/switch_t1.py
index 63ac57c..e65d1a4 100644
--- a/zhaquirks/xiaomi/aqara/switch_t1.py
+++ b/zhaquirks/xiaomi/aqara/switch_t1.py
@@ -40,7 +40,6 @@ from zhaquirks.xiaomi.aqara.opple_remote import MultistateInputCluster
 class OppleCluster(XiaomiAqaraE1Cluster):
     """Xiaomi Aqara T1 relay cluster."""
 
-    ep_attribute = "opple_cluster"
     attributes = {
         0x000A: ("switch_type", t.uint8_t, True),
         0x0201: ("power_outage_memory", t.Bool, True),
diff --git a/zhaquirks/xiaomi/aqara/thermostat_agl001.py b/zhaquirks/xiaomi/aqara/thermostat_agl001.py
index c06ca51..4085078 100644
--- a/zhaquirks/xiaomi/aqara/thermostat_agl001.py
+++ b/zhaquirks/xiaomi/aqara/thermostat_agl001.py
@@ -126,8 +126,6 @@ class ThermostatCluster(CustomCluster, Thermostat):
 class AqaraThermostatSpecificCluster(XiaomiAqaraE1Cluster):
     """Aqara manufacturer specific settings."""
 
-    ep_attribute = "opple_cluster"
-
     attributes = XiaomiAqaraE1Cluster.attributes.copy()
     attributes.update(
         {
diff --git a/zhaquirks/xiaomi/aqara/tvoc.py b/zhaquirks/xiaomi/aqara/tvoc.py
index 34058e1..7e585c8 100644
--- a/zhaquirks/xiaomi/aqara/tvoc.py
+++ b/zhaquirks/xiaomi/aqara/tvoc.py
@@ -79,7 +79,6 @@ class TVOCDisplayUnit(t.enum_factory(t.uint8_t)):
 class TVOCCluster(XiaomiAqaraE1Cluster):
     """Aqara LUMI Config cluster."""
 
-    ep_attribute = "aqara_cluster"
     attributes = {
         DISPLAY_UNIT: ("display_unit", TVOCDisplayUnit, True),
     }


Test patch:
diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py
index 05209bc..c8a4c97 100644
--- a/tests/test_xiaomi.py
+++ b/tests/test_xiaomi.py
@@ -49,7 +49,6 @@ from zhaquirks.xiaomi import (
     XIAOMI_AQARA_ATTRIBUTE_E1,
     XIAOMI_NODE_DESC,
     BasicCluster,
-    XiaomiAqaraE1Cluster,
     XiaomiCustomDevice,
     XiaomiQuickInitDevice,
     handle_quick_init,
@@ -1341,7 +1340,7 @@ async def test_xiaomi_t1_door_sensor(
     assert on_off_listener.attribute_updates[1][0] == OnOff.AttributeDefs.on_off.id
     assert on_off_listener.attribute_updates[1][1] == t.Bool.false
 
-    opple_cluster = device.endpoints[1].in_clusters[XiaomiAqaraE1Cluster.cluster_id]
+    opple_cluster = device.endpoints[1].opple_cluster
     ClusterListener(opple_cluster)
 
     power_cluster = device.endpoints[1].power
@@ -1422,7 +1421,7 @@ async def test_xiaomi_e1_driver_light_level(
     """Test Aqara E1 driver light level cluster conversion."""
     device = zigpy_device_from_quirk(zhaquirks.xiaomi.aqara.driver_curtain_e1.DriverE1)
 
-    opple_cluster = device.endpoints[1].in_clusters[XiaomiAqaraE1Cluster.cluster_id]
+    opple_cluster = device.endpoints[1].opple_cluster
     opple_listener = ClusterListener(opple_cluster)
     opple_zcl_iilluminance_id = 0x0429
 
@@ -1510,3 +1509,33 @@ async def test_xiaomi_e1_roller_commands_2(zigpy_device_from_quirk, command, val
     assert (
         analog_cluster._write_attributes.call_args[0][0][0].value.value == 100 - value
     )
+
+
+def test_aqara_acn003_signature_match(assert_signature_matches_quirk):
+    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=4447, 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": {
+            "1": {
+                "profile_id": 0x0104,
+                "device_type": "0x0102",
+                "in_clusters": [
+                    "0x0000",
+                    "0x0003",
+                    "0x0004",
+                    "0x0005",
+                    "0x0006",
+                    "0x0008",
+                    "0x0300",
+                    "0xfcc0",
+                ],
+                "out_clusters": ["0x000a", "0x0019"],
+            }
+        },
+        "manufacturer": "Aqara",
+        "model": "lumi.light.acn003",
+        "class": "aqara_light.LumiLightAcn003",
+    }
+
+    assert_signature_matches_quirk(
+        zhaquirks.xiaomi.aqara.light_acn.LumiLightAcn003, signature
+    )