Repository: zigpy/zha-device-handlers
Pull request #2609 (2023-09-28 15:11:27)

Add IKEA Rodret dimmer support
## Proposed change
Added signature to match Ikea's Rodret dimmer button.

## Additional information
Rodret is basically same as Tradfri without alarm input cluster and without window covering output cluster. Tested as a standalone quirk

## Checklist
<!--
  Put an 'x' in all boxes that apply.
  Note: You do not need to tick all boxes before creating a PR.
-->

- [ ] 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/2609?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) Report
All modified lines are covered by tests :white_check_mark:
> Comparison is base [(`7385465`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/7385465bd6f04f295893fb20d053433011183935?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 86.57% compared to head [(`35494e4`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2609?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 86.58%.

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


```diff
@@           Coverage Diff           @@
##              dev    #2609   +/-   ##
=======================================
  Coverage   86.57%   86.58%           
=======================================
  Files         277      277           
  Lines        8523     8527    +4     
=======================================
+ Hits         7379     7383    +4     
  Misses       1144     1144           
```


| [Files](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2609?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) | Coverage Δ | |
|---|---|---|
| [zhaquirks/ikea/twobtnremote.py](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2609?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL2lrZWEvdHdvYnRucmVtb3RlLnB5) | `100.00% <100.00%> (ø)` | |


</details>

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

MattWestb: You need putting one more empty line between the old last class and the new one (shall being 2 lines is only one) (Line 198) then the test shall working OK.

Patch:
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 11d7428..976a847 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -16,7 +16,7 @@ jobs:
       # - No PRs marked as no-stale
       # - No issues marked as no-stale or help-wanted
       - name: 180 days stale issues & PRs policy
-        uses: actions/stale@v7
+        uses: actions/stale@v8
         with:
           repo-token: ${{ secrets.GITHUB_TOKEN }}
           days-before-stale: 180
@@ -49,7 +49,7 @@ jobs:
       # - No Issues marked as no-stale or help-wanted
       # - No PRs (-1)
       - name: Needs more information stale issues policy
-        uses: actions/stale@v7
+        uses: actions/stale@v8
         with:
           repo-token: ${{ secrets.GITHUB_TOKEN }}
           only-labels: "needs more information"
diff --git a/README.md b/README.md
index b7f619a..6539114 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # ZHA Device Handlers For Home Assistant
 
 ![CI](https://github.com/zigpy/zha-device-handlers/workflows/CI/badge.svg?branch=dev)
-[![Coverage Status](https://codecov.io/gh/zigpy/zha-device-handlers/branch/dev/graph/badge.svg)](https://codecov.io/gh/zigpy/zha-device-handlers)
+[![Coverage Status](https://codecov.io/gh/zigpy/zha-device-handlers/branch/dev/graph/badge.svg)](https://app.codecov.io/gh/zigpy/zha-device-handlers/tree/dev)
 
 ZHA Device Handlers are custom quirks implementations for [Zigpy](https://github.com/zigpy/zigpy), the library that provides the [Zigbee](http://www.zigbee.org) support for the [ZHA](https://www.home-assistant.io/components/zha/) component in [Home Assistant](https://www.home-assistant.io).
 
@@ -13,31 +13,31 @@ Custom quirks implementations for zigpy implemented as ZHA Device Handlers are a
 
 ## Primer
 
-ZHA device handlers and it's provided Quirks allow Zigpy, ZHA and Home Assistant to work with non standard Zigbee devices. If you are reading this you may have a device that isn't working as expected. This can be the case for a number of reasons but in this guide we will cover the cases where functionality is provided by a device in a non specification compliant manner by the device manufacturer.
+ZHA device handlers and its provided Quirks allow Zigpy, ZHA and Home Assistant to work with non-standard Zigbee devices. If you are reading this you may have a device that isn't working as expected. This can be the case for a number of reasons but in this guide we will cover the cases where functionality is provided by a device in a non specification compliant manner by the device manufacturer.
 
 ## What are these specifications
 
 Reference official Zigbee specification documentation from Connectivity Standards Alliance (a.k.a. "CSA-IOT", formerly "Zigbee Alliance"):
 
-- Zigbee Protocol Specification (also known as "Zigbee Pro" specifications)
-  - [Zigbee Protocol Specification 2023 (also known as "Zigbee PRO 2023" or just Zigbee R23)](https://csa-iot.org/wp-content/uploads/2023/04/05-3474-23-csg-zigbee-specification-compressed.pdf)
-  - [Zigbee Protocol Specification 2017 (also known as "Zigbee PRO 2017" or just Zigbee R22)](https://csa-iot.org/wp-content/uploads/2022/01/docs-05-3474-22-0csg-zigbee-specification-1.pdf)
-  - [Zigbee Protocol Specification 2015 (also known as "Zigbee PRO 2015" or just Zigbee R21)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-05-3474-21-0csg-zigbee-specification.pdf)
 - Zigbee Cluster Library Specification
   - [Zigbee Cluster Library Specification R8 (Revision 8)](https://zigbeealliance.org/wp-content/uploads/2021/10/07-5123-08-Zigbee-Cluster-Library.pdf)
-  - [Zigbee Cluster Library Specification R7 (Revision 7)](https://github.com/Koenkk/zigbee-herdsman/blob/master/docs/Zigbee%20Cluster%20Library%20Specification%20v7.pdf)
+  - [Zigbee Cluster Library Specification R7 (Revision 7)](https://zigbeealliance.org/wp-content/uploads/2021/10/07-5123-07-ZigbeeClusterLibrary_Revision_7-1.pdf)
   - [Zigbee Cluster Library Specification R6 (Revision 6)](https://zigbeealliance.org/wp-content/uploads/2019/12/07-5123-06-zigbee-cluster-library-specification.pdf)
+- Zigbee Protocol Specification (also known as "Zigbee Pro" specifications)
+  - [Zigbee Protocol Specification 2023 (also known as "Zigbee PRO 2023" / Zigbee R23)](https://csa-iot.org/wp-content/uploads/2023/04/05-3474-23-csg-zigbee-specification-compressed.pdf)
+  - [Zigbee Protocol Specification 2017 (also known as "Zigbee PRO 2017" / Zigbee R22)](https://csa-iot.org/wp-content/uploads/2022/01/docs-05-3474-22-0csg-zigbee-specification-1.pdf)
+  - [Zigbee Protocol Specification 2015 (also known as "Zigbee PRO 2015" / Zigbee R21)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-05-3474-21-0csg-zigbee-specification.pdf)
 - Zigbee Device Specifications
-  - [Zigbee Base Device Behavior Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/2019/12/docs-13-0402-13-00zi-Base-Device-Behavior-Specification-2-1.pdf)
-  - [Zigbee Lighting & Occupancy Device Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-15-0014-05-0plo-Lighting-OccupancyDevice-Specification-V1.0.pdf)
-- ZigBee Green Power (ZGP "GreenPower" Profile) specifications
-  - [Zigbee PRO Green Power feature specification Basic functionality set (v 1.1.1)](https://csa-iot.org/wp-content/uploads/2022/01/docs-14-0563-18-batt-Green-Power-Basic-specification-v1.1.1.pdf)
-  - [Zigbee PRO Green Power feature Specification 1.0a (Revision 26)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-09-5499-26-batt-zigbee-green-power-specification.pdf)
-- ZigBee Smart Energy (ZSE / Zigbee SE "Smart Energy" Profile) specifications
+  - [Zigbee Base Device Behavior Specification (v1.0)](https://zigbeealliance.org/wp-content/uploads/2019/12/docs-13-0402-13-00zi-Base-Device-Behavior-Specification-2-1.pdf)
+  - [Zigbee Lighting & Occupancy Device Specification (v1.0)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-15-0014-05-0plo-Lighting-OccupancyDevice-Specification-V1.0.pdf)
+- Zigbee Green Power (ZGP "GreenPower" Profile) specifications
+  - [Zigbee PRO Green Power feature specification Basic functionality set (v1.1.1)](https://csa-iot.org/wp-content/uploads/2022/01/docs-14-0563-18-batt-Green-Power-Basic-specification-v1.1.1.pdf)
+  - [Zigbee PRO Green Power feature specification 1.0a (Revision 26)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-09-5499-26-batt-zigbee-green-power-specification.pdf)
+- Zigbee Smart Energy (ZSE / Zigbee SE "Smart Energy" Profile) specifications
   - Zigbee Smart Energy Standard 1.4
   - [ZigBee Smart Energy Standard (v1.2a)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-07-5356-19-0zse-zigbee-smart-energy-profile-specification.pdf)
 
-In additional you can also reference third-party and manufacturer specific documentation:
+Additionally, see these third-party and manufacturer specific documents:
 
 - [Tuya - Zigbee Connection Standard (Tuya Smart Documentation)](https://github.com/Koenkk/zigbee-herdsman/blob/master/docs/Zigbee%20Connection%20Standard_Tuya%20Smart_Documentation.pdf)
   - [Zigbee2MQTT guide on understanding the custom 'manuSpecificTuya' cluster that TuYa devices uses](https://www.zigbee2mqtt.io/advanced/support-new-devices/02_support_new_tuya_devices.html)
@@ -49,13 +49,13 @@ In additional you can also reference third-party and manufacturer specific docum
 
 A device is a physical object that you want to join to a Zigbee network: a light bulb, a switch, a sensor etc. The host application, in this case Zigpy, needs to understand how to interact with the device so there are standards that define how the application and devices can communicate. The device's functionality is described by several **descriptors** while the device itself contains **endpoints** and **endpoints** contain **clusters**. There are two types of clusters an endpoint contains:
 
-- **in_clusters** - are "Server" clusters in ZCL terms. These clusters control the device, e.g. a smart plug or light bulb would have an `on_off` server cluster. **in_clusters** are also the ones which also send attribute reports and/or you can read an attribute from a **in_cluster**.
+- **in_clusters** - are "Server" clusters in ZCL terms. These clusters control the device, e.g. a smart plug or light bulb would have an `on_off` server cluster. **in_clusters** are also the ones which also send attribute reports and/or you can read an attribute from an **in_cluster**.
 - **out_clusters** - are "Client" clusters. These clusters control some other device, as "Client" cluster sends commands to "Server" cluster. For example an On/Off remote would have an `on_off` client cluster and will generate cluster commands and send those to some other device.
   Zigpy needs to understand all these elements in order to correctly work with the device.
 
 ### Endpoints
 
-Endpoints are essentially groupings of functionality. For example, a typical Zigbee light bulb will have a single endpoint for the light. A multi-gang wall switch may have an endpoint for each individual switch so they can all be controlled separately. Each endpoint has several functions represented by clusters.
+Endpoints are essentially groupings of functionality. For example, a typical Zigbee light bulb will have a single endpoint for the light. A multi-gang wall switch may have an endpoint for each individual switch, so they can all be controlled separately. Each endpoint has several functions represented by clusters.
 
 ### Clusters
 
@@ -67,7 +67,7 @@ For the purposes of Zigpy and Quirks we will focus on two descriptors: **Node De
 
 #### Node Descriptor
 
-A node descriptor explains some basic device attributes to Zigpy. The manufacturer code and the power type are the ones that we generally care about. In most cases you won't have to worry about this but it is good to know why it is there in case you come across it while looking at an existing quirk. Here is an example:
+A node descriptor explains some basic device attributes to Zigpy. The manufacturer code and the power type are the ones that we generally care about. In most cases you won't have to worry about this, but it is good to know why it is there in case you come across it while looking at an existing quirk. Here is an example:
 `<Optional byte1=2 byte2=64 mac_capability_flags=128 manufacturer_code=4174 maximum_buffer_size=82 maximum_incoming_transfer_size=82 server_mask=0 maximum_outgoing_transfer_size=82 descriptor_capability_field=0>`
 
 #### Simple Descriptor
@@ -77,7 +77,7 @@ A simple descriptor is a description of a Zigbee device endpoint and is responsi
 
 ## What the heck is a quirk
 
-In human terms you can think of a quirk like google translate. I know it's a weird comparison but lets dig in a bit. You may only speak one language but there is an interesting article written in another language that you really want to read. Google translate takes the original article and displays it in a format (language) that you understand. A quirk is a file that translates device functionality from the format that the manufacturer chose to implement it in to a format that Zigpy and in turn ZHA understand. The main purpose of a quirk is to serve as a translator. A quirk is comprised of several parts:
+In human terms you can think of a quirk like Google Translate. I know it's a weird comparison but let's dig in a bit. You may only speak one language but there is an interesting article written in another language that you really want to read. Google Translate takes the original article and displays it in a format (language) that you understand. A quirk is a file that translates device functionality from the format that the manufacturer chose to implement it in to a format that Zigpy and in turn ZHA understand. The main purpose of a quirk is to serve as a translator. A quirk comprises several parts:
 
 - Signature - To identify and apply the correct quirk
 - Replacement - To allow Zigpy and ZHA to correctly work with the device
@@ -123,7 +123,7 @@ signature = {
 
 ### Replacement
 
-The replacement on a quirk is what we want the device to be. Remember, we said that quirks were like Google translate... you can think of the replacement like the output from Google translate. The replacement dict is what will actually be used by Zigpy and ZHA to interact with the device. The structure of `replacement` is the same as signature with 2 key differences: `models_info` is generally omitted and there is an extra element `skip_configuration` that instructs the application to skip configuration if necessary. Some manufacturers have not implemented the specifications correctly and the devices come pre-configured and therefore the configuration calls fail (non Zigbee 3.0 Xiaomi devices for instance). Usually, you should not add `skip_configuration`.
+The replacement on a quirk is what we want the device to be. Remember, we said that quirks were like Google Translate... You can think of the replacement like the output from Google Translate. The replacement dict is what will actually be used by Zigpy and ZHA to interact with the device. The structure of `replacement` is the same as signature with 2 key differences: `models_info` is generally omitted and there is an extra element `skip_configuration` that instructs the application to skip configuration if necessary. Some manufacturers have not implemented the specifications correctly and the devices come pre-configured and therefore the configuration calls fail (non Zigbee 3.0 Xiaomi devices for instance). Usually, you should not add `skip_configuration`.
 
 Here is an example:
 
@@ -567,20 +567,8 @@ def test_ts0121_signature(assert_signature_matches_quirk):
 
 ## Zigpy
 
-**[zigpy](https://github.com/zigpy/zigpy)** is **[Zigbee protocol stack](https://en.wikipedia.org/wiki/Zigbee)** integration project to implement the **[Zigbee Home Automation](https://www.zigbee.org/)** standard as a Python 3 library. Zigbee Home Automation integration with zigpy allows you to connect one of many off-the-shelf Zigbee adapters using one of the available Zigbee radio library modules compatible with zigpy to control Zigbee based devices. There is currently support for controlling Zigbee device types such as binary sensors (e.g., motion and door sensors), sensors (e.g., temperature sensors), lightbulbs, switches, locks, fans, covers (blinds, marquees, and more). A working implementation of zigpy exist in **[Home Assistant](https://www.home-assistant.io)** (Python based open source home automation software) as part of its **[ZHA component](https://www.home-assistant.io/components/zha/)**
-
-## ZHA Map
-
-[zha-map](https://github.com/zha-ng/zha-map) project allow building a Zigbee network topology map for ZHA component in Home Assistant.
-
-## zha-network-visualization-card
-
-[zha-network-visualization-card](https://github.com/dmulcahey/zha-network-visualization-card) is a custom Lovelace element for visualizing the Zigbee network map for ZHA component in Home Assistant.
+**[zigpy](https://github.com/zigpy/zigpy)** is a **[Zigbee protocol stack](https://en.wikipedia.org/wiki/Zigbee)** integration project to implement the **[Zigbee Home Automation](https://www.zigbee.org/)** standard as a Python 3 library. Zigbee Home Automation integration with zigpy allows you to connect one of many off-the-shelf Zigbee adapters using one of the available Zigbee radio library modules compatible with zigpy to control Zigbee based devices. There is currently support for controlling Zigbee device types such as binary sensors (e.g., motion and door sensors), sensors (e.g., temperature sensors), light bulbs, switches, locks, fans, covers (blinds, marquees, and more). A working implementation of zigpy exists in **[Home Assistant](https://www.home-assistant.io)** (Python based open source home automation software) as part of its **[ZHA component](https://www.home-assistant.io/components/zha/)**
 
 ## ZHA Network Card
 
 [zha-network-card](https://github.com/dmulcahey/zha-network-card) is a custom Lovelace card that displays ZHA network and device information in Home Assistant
-
-## zigpy-deconz-parser
-
-[zigpy-deconz-parser](https://github.com/zha-ng/zigpy-deconz-parser) project can parse Home Assistant ZHA component debug log using `zigpy-deconz` library if you have ConBee or RaspBee hardware.
diff --git a/zhaquirks/gledopto/glsd001.py b/zhaquirks/gledopto/glsd_dimmer.py
similarity index 88%
rename from zhaquirks/gledopto/glsd001.py
rename to zhaquirks/gledopto/glsd_dimmer.py
index 4f9d31c..824ff50 100644
--- a/zhaquirks/gledopto/glsd001.py
+++ b/zhaquirks/gledopto/glsd_dimmer.py
@@ -1,4 +1,4 @@
-"""Quirk for GLEDOPTO GL-SD-001."""
+"""Quirk for GLEDOPTO GL-SD-* dimmers."""
 
 from zigpy.profiles import zha
 from zigpy.quirks import CustomCluster, CustomDevice
@@ -30,11 +30,11 @@ class LevelControlNoReply(NoReplyMixin, CustomCluster, LevelControl):
     void_input_commands = {cmd.id for cmd in LevelControl.commands_by_name.values()}
 
 
-class GledoptoGlSd001(CustomDevice):
-    """Gledopto GL-SD-001 dimmer custom device implementation."""
+class GledoptoGlSdDimmer(CustomDevice):
+    """Gledopto GL-SD-001 & GL-SD-003P dimmer custom device implementation."""
 
     signature = {
-        MODELS_INFO: [("GLEDOPTO", "GL-SD-001")],
+        MODELS_INFO: [("GLEDOPTO", "GL-SD-001"), ("GLEDOPTO", "GL-SD-003P")],
         ENDPOINTS: {
             1: {
                 PROFILE_ID: zha.PROFILE_ID,
diff --git a/zhaquirks/ikea/__init__.py b/zhaquirks/ikea/__init__.py
index 50a7bd5..28d2f73 100644
--- a/zhaquirks/ikea/__init__.py
+++ b/zhaquirks/ikea/__init__.py
@@ -143,6 +143,16 @@ class ShortcutV2Cluster(EventableCluster):
 
 
 # ZCL compliant IKEA power configuration clusters:
+class PowerConfig1AAACluster(CustomCluster, PowerConfiguration):
+    """Updating power attributes: 2 AAA."""
+
+    _CONSTANT_ATTRIBUTES = {
+        BATTERY_SIZE: 4,
+        BATTERY_QUANTITY: 1,
+        BATTERY_RATED_VOLTAGE: 15,
+    }
+
+
 class PowerConfig2AAACluster(CustomCluster, PowerConfiguration):
     """Updating power attributes: 2 AAA."""
 
diff --git a/zhaquirks/ikea/twobtnremote.py b/zhaquirks/ikea/twobtnremote.py
index 054ac23..cfd8918 100644
--- a/zhaquirks/ikea/twobtnremote.py
+++ b/zhaquirks/ikea/twobtnremote.py
@@ -40,7 +40,12 @@ from zhaquirks.const import (
     TURN_OFF,
     TURN_ON,
 )
-from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID, DoublingPowerConfig1CRCluster
+from zhaquirks.ikea import (
+    IKEA,
+    IKEA_CLUSTER_ID,
+    DoublingPowerConfig1CRCluster,
+    PowerConfig1AAACluster,
+)
 
 
 class IkeaTradfriRemote2Btn(CustomDevice):
@@ -195,3 +200,64 @@ class IkeaTradfriRemote2BtnZLL(CustomDevice):
     }
 
     device_automation_triggers = IkeaTradfriRemote2Btn.device_automation_triggers.copy()
+
+
+class IkeaRodretRemote2Btn(CustomDevice):
+    """Custom device representing IKEA of Sweden RODRET remote control."""
+
+    signature = {
+        # <SimpleDescriptor endpoint=1 profile=260 device_type=2080
+        # device_version=1
+        # input_clusters=[0, 1, 3, 32, 4096, 64636]
+        # output_clusters=[3, 4, 6, 8, 258, 4096]>
+        MODELS_INFO: [(IKEA, "RODRET Dimmer")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    PollControl.cluster_id,
+                    LightLink.cluster_id,
+                    IKEA_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Ota.cluster_id,
+                    LightLink.cluster_id,
+                ],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfig1AAACluster,
+                    Identify.cluster_id,
+                    PollControl.cluster_id,
+                    LightLink.cluster_id,
+                    IKEA_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Ota.cluster_id,
+                    LightLink.cluster_id,
+                ],
+            }
+        }
+    }
+
+    device_automation_triggers = IkeaTradfriRemote2Btn.device_automation_triggers.copy()
diff --git a/zhaquirks/inovelli/__init__.py b/zhaquirks/inovelli/__init__.py
index 7cee57f..f00da77 100644
--- a/zhaquirks/inovelli/__init__.py
+++ b/zhaquirks/inovelli/__init__.py
@@ -273,7 +273,7 @@ class Inovelli_VZM35SN_Cluster(Inovelli_VZM31SN_Cluster):
             0x001E: ("non_neutral_aux_med_gear_learn_value", t.uint8_t, True),
             0x001F: ("non_neutral_aux_low_gear_learn_value", t.uint8_t, True),
             0x0034: ("smart_fan_mode", t.Bool, True),
-            0x0106: ("smart_fan_led_display_levels", t.uint8_t, True),
+            0x0107: ("smart_fan_led_display_levels", t.uint8_t, True),
         }
     )
 
diff --git a/zhaquirks/quirk_ids.py b/zhaquirks/quirk_ids.py
new file mode 100644
index 0000000..36f4213
--- /dev/null
+++ b/zhaquirks/quirk_ids.py
@@ -0,0 +1,10 @@
+"""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
+
+# Xiaomi
+XIAOMI_AQARA_VIBRATION_AQ1 = (
+    "xiaomi.aqara_vibration_aq1"  # vibration sensor with custom cluster handler
+)
diff --git a/zhaquirks/tuya/air/ts0601_air_quality.py b/zhaquirks/tuya/air/ts0601_air_quality.py
index dafd9b2..790f261 100644
--- a/zhaquirks/tuya/air/ts0601_air_quality.py
+++ b/zhaquirks/tuya/air/ts0601_air_quality.py
@@ -82,6 +82,7 @@ class TuyaCO2SensorGPP(CustomDevice):
         # input_clusters=[0, 4, 5, 61184],
         # output_clusters=[25, 10])
         MODELS_INFO: [
+            ("_TZE200_mja3fuja", "TS0601"),
             ("_TZE200_ryfmq5rl", "TS0601"),
             ("_TZE200_yvx5lh6k", "TS0601"),
             ("_TZE200_dwcarsat", "TS0601"),
diff --git a/zhaquirks/tuya/mcu/__init__.py b/zhaquirks/tuya/mcu/__init__.py
index 6c70e91..97ce0de 100644
--- a/zhaquirks/tuya/mcu/__init__.py
+++ b/zhaquirks/tuya/mcu/__init__.py
@@ -434,6 +434,56 @@ class TuyaOnOffManufCluster(TuyaMCUCluster):
             "on_off",
             endpoint_id=6,
         ),
+        0x65: DPToAttributeMapping(
+            TuyaOnOff.ep_attribute,
+            "on_off",
+            endpoint_id=7,
+        ),
+        0x66: DPToAttributeMapping(
+            TuyaOnOff.ep_attribute,
+            "on_off",
+            endpoint_id=8,
+        ),
+        0x67: DPToAttributeMapping(
+            TuyaOnOff.ep_attribute,
+            "on_off",
+            endpoint_id=9,
+        ),
+        0x68: DPToAttributeMapping(
+            TuyaOnOff.ep_attribute,
+            "on_off",
+            endpoint_id=10,
+        ),
+        0x69: DPToAttributeMapping(
+            TuyaOnOff.ep_attribute,
+            "on_off",
+            endpoint_id=11,
+        ),
+        0x6A: DPToAttributeMapping(
+            TuyaOnOff.ep_attribute,
+            "on_off",
+            endpoint_id=12,
+        ),
+        0x6B: DPToAttributeMapping(
+            TuyaOnOff.ep_attribute,
+            "on_off",
+            endpoint_id=13,
+        ),
+        0x6C: DPToAttributeMapping(
+            TuyaOnOff.ep_attribute,
+            "on_off",
+            endpoint_id=14,
+        ),
+        0x6D: DPToAttributeMapping(
+            TuyaOnOff.ep_attribute,
+            "on_off",
+            endpoint_id=15,
+        ),
+        0x6E: DPToAttributeMapping(
+            TuyaOnOff.ep_attribute,
+            "on_off",
+            endpoint_id=16,
+        ),
     }
 
     data_point_handlers = {
@@ -443,6 +493,16 @@ class TuyaOnOffManufCluster(TuyaMCUCluster):
         4: "_dp_2_attr_update",
         5: "_dp_2_attr_update",
         6: "_dp_2_attr_update",
+        0x65: "_dp_2_attr_update",
+        0x66: "_dp_2_attr_update",
+        0x67: "_dp_2_attr_update",
+        0x68: "_dp_2_attr_update",
+        0x69: "_dp_2_attr_update",
+        0x6A: "_dp_2_attr_update",
+        0x6B: "_dp_2_attr_update",
+        0x6C: "_dp_2_attr_update",
+        0x6D: "_dp_2_attr_update",
+        0x6E: "_dp_2_attr_update",
     }
 
 
diff --git a/zhaquirks/tuya/ts000f_switch.py b/zhaquirks/tuya/ts000f_switch.py
new file mode 100644
index 0000000..ca92b81
--- /dev/null
+++ b/zhaquirks/tuya/ts000f_switch.py
@@ -0,0 +1,99 @@
+"""tuya TS000F xkap8wtb On/Off In-line Switches with metering support."""
+
+from zigpy.profiles import zgp, zha
+from zigpy.zcl.clusters.general import (
+    Basic,
+    GreenPowerProxy,
+    Groups,
+    Identify,
+    OnOff,
+    Ota,
+    Scenes,
+    Time,
+)
+from zigpy.zcl.clusters.lightlink import LightLink
+
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODEL,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+from zhaquirks.tuya import (
+    TuyaZB1888Cluster,
+    TuyaZBE000Cluster,
+    TuyaZBElectricalMeasurement,
+    TuyaZBMeteringCluster,
+    TuyaZBOnOffAttributeCluster,
+)
+from zhaquirks.tuya.mcu import EnchantedDevice
+
+
+class Tuya_1G_Wall_Switch_Metering(EnchantedDevice):
+    """Tuya 1 gang wall switch with metering and restore power state support."""
+
+    signature = {
+        MODEL: "TS000F",
+        ENDPOINTS: {
+            # SizePrefixedSimpleDescriptor(endpoint=242, profile=41440, device_type=97, device_version=0, input_clusters=[], output_clusters=[33])
+            # input_clusters=["0x0000","0x0003","0x0004","0x0005","0x0006", "0x000a", "0x0702","0x0b04","0x1000","0x1888","0xe000"]
+            # output_clusters=["0x0019"]>
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    Time.cluster_id,
+                    TuyaZBMeteringCluster.cluster_id,
+                    TuyaZBElectricalMeasurement.cluster_id,
+                    LightLink.cluster_id,
+                    TuyaZB1888Cluster.cluster_id,
+                    TuyaZBE000Cluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+            # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
+            # input_clusters=[]
+            # output_clusters=["0x0021"]>
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaZBOnOffAttributeCluster,
+                    Time.cluster_id,
+                    TuyaZBMeteringCluster,
+                    TuyaZBElectricalMeasurement,
+                    LightLink.cluster_id,
+                    TuyaZB1888Cluster,
+                    TuyaZBE000Cluster,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
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..4bb55cf 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
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -650,7 +663,7 @@ class Plug_TZ3210_2AC(CustomDevice):
                     Identify.cluster_id,
                     Groups.cluster_id,
                     Scenes.cluster_id,
-                    OnOff.cluster_id,
+                    TuyaZBOnOffAttributeCluster,
                     TuyaZBMeteringCluster,
                     TuyaZBElectricalMeasurement,
                     TuyaNewManufCluster,
@@ -664,7 +677,7 @@ class Plug_TZ3210_2AC(CustomDevice):
                     Identify.cluster_id,
                     Groups.cluster_id,
                     Scenes.cluster_id,
-                    OnOff.cluster_id,
+                    TuyaZBOnOffAttributeCluster,
                     TuyaNewManufCluster,
                 ],
                 OUTPUT_CLUSTERS: [],
@@ -682,6 +695,8 @@ class Plug_TZ3210_2AC(CustomDevice):
 class Plug_TZ3210_1AC(CustomDevice):
     """TS0011F 1 outlet plug."""
 
+    quirk_id = TUYA_PLUG_ONOFF
+
     signature = {
         MODEL: "TS011F",
         ENDPOINTS: {
@@ -727,7 +742,7 @@ class Plug_TZ3210_1AC(CustomDevice):
                     Identify.cluster_id,
                     Groups.cluster_id,
                     Scenes.cluster_id,
-                    OnOff.cluster_id,
+                    TuyaZBOnOffAttributeCluster,
                     TuyaZBMeteringCluster,
                     TuyaZBElectricalMeasurement,
                     TuyaNewManufCluster,
@@ -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_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py
index 0a24e1d..ed9afbf 100644
--- a/zhaquirks/tuya/ts0601_dimmer.py
+++ b/zhaquirks/tuya/ts0601_dimmer.py
@@ -208,6 +208,7 @@ class TuyaDoubleSwitchDimmerGP(TuyaDimmerSwitch):
         MODELS_INFO: [
             ("_TZE200_fjjbhx9d", "TS0601"),
             ("_TZE200_gwkapsoq", "TS0601"),  # Loratap
+            ("_TZE204_zenj4lxv", "TS0601"),
         ],
         ENDPOINTS: {
             # <SimpleDescriptor endpoint=1 profile=260 device_type=0x0100
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 b40b60d..1f8717f 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
@@ -122,6 +127,7 @@ class TuyaSingleSwitch_GP(TuyaSwitch):
     signature = {
         MODELS_INFO: [
             ("_TZE200_gbagoilo", "TS0601"),  # reported in #1634
+            ("_TZE204_6fk3gewc", "TS0601"),
         ],
         ENDPOINTS: {
             # <SimpleDescriptor endpoint=1 profile=260 device_type=51 device_version=1
@@ -176,6 +182,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 +239,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 +307,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 +365,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 +441,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 +590,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 +674,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"),
@@ -748,3 +768,293 @@ class TuyaSextupleSwitchTO_GP(TuyaSwitch):
             },
         }
     }
+
+
+class TuyaSwitchX8_GP(TuyaSwitch):
+    """Tuya x8 channels switch with GreenPowerProxy cluster device."""
+
+    signature = {
+        MODELS_INFO: [
+            ("_TZE200_wktrysab", "TS0601"),
+        ],
+        ENDPOINTS: {
+            # <SimpleDescriptor endpoint=1 profile=260 device_type=81
+            # input_clusters=[0x0000,0x0004,0x0005,0xEF00]
+            # output_clusters=[0x000A,0x0019]>
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaOnOffManufCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
+            # input_clusters=[]
+            # output_clusters=[33]
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaOnOffManufCluster,
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            3: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            4: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            5: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            6: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            7: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            8: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        }
+    }
+
+
+class TuyaSwitchX16_GP(TuyaSwitch):
+    """Tuya x16 channels switch with GreenPowerProxy cluster device."""
+
+    signature = {
+        MODELS_INFO: [
+            ("_TZE204_vmcgja59", "TS0601"),
+        ],
+        ENDPOINTS: {
+            # <SimpleDescriptor endpoint=1 profile=260 device_type=81
+            # input_clusters=[0x0000,0x0004,0x0005,0xEF00]
+            # output_clusters=[0x000A,0x0019]>
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaOnOffManufCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
+            # input_clusters=[]
+            # output_clusters=[33]
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaOnOffManufCluster,
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            3: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            4: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            5: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            6: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            7: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            8: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            9: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            10: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            11: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            12: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            13: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            14: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            15: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            16: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    TuyaOnOffNM,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        }
+    }
diff --git a/zhaquirks/tuya/ts0601_valve.py b/zhaquirks/tuya/ts0601_valve.py
index d37911b..e5e6936 100644
--- a/zhaquirks/tuya/ts0601_valve.py
+++ b/zhaquirks/tuya/ts0601_valve.py
@@ -51,6 +51,7 @@ class TuyaValveManufCluster(TuyaMCUCluster):
             0xEF02: ("state", t.enum8, True),
             0xEF03: ("last_valve_open_duration", t.uint32_t, True),
             0xEF04: ("dp_6", t.uint32_t, True),
+            0xEF05: ("valve_position", t.uint32_t, True),
         }
     )
 
@@ -83,6 +84,10 @@ class TuyaValveManufCluster(TuyaMCUCluster):
             TuyaMCUCluster.ep_attribute,
             "last_valve_open_duration",
         ),
+        102: DPToAttributeMapping(
+            TuyaMCUCluster.ep_attribute,
+            "valve_position",
+        ),
     }
 
     data_point_handlers = {
@@ -93,6 +98,7 @@ class TuyaValveManufCluster(TuyaMCUCluster):
         11: "_dp_2_attr_update",
         12: "_dp_2_attr_update",
         15: "_dp_2_attr_update",
+        102: "_dp_2_attr_update",
     }
 
 
@@ -137,6 +143,45 @@ class TuyaValve(CustomDevice):
     }
 
 
+class BasicTuyaValve(CustomDevice):
+    """Basic Tuya valve device."""
+
+    signature = {
+        MODELS_INFO: [("_TZE200_1n2zev06", "TS0601")],
+        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=81, device_version=1,
+        # input_clusters=[0, 4, 5, 61184], output_clusters=[25, 10])
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaValveManufCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaOnOff,
+                    TuyaValveManufCluster,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            }
+        }
+    }
+
+
 class ParksideTuyaValveManufCluster(TuyaMCUCluster):
     """Manufacturer Specific Cluster for the _TZE200_htnnfasr water valve sold as PARKSIDE."""
 
diff --git a/zhaquirks/xbee/__init__.py b/zhaquirks/xbee/__init__.py
index 98068a3..470d269 100644
--- a/zhaquirks/xbee/__init__.py
+++ b/zhaquirks/xbee/__init__.py
@@ -629,6 +629,8 @@ class XBeeCommon(CustomDevice):
 
     def deserialize(self, endpoint_id, cluster_id, data):
         """Deserialize."""
+        if endpoint_id == 0:
+            return super().deserialize(endpoint_id, cluster_id, data)
         tsn = self._application.get_sequence()
         command_id = 0x0000
         hdr = foundation.ZCLHeader.cluster(tsn, command_id)
diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py
index c945b36..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):
@@ -457,7 +457,7 @@ class BinaryOutputInterlock(CustomCluster, BinaryOutput):
 
 
 class XiaomiPowerConfiguration(PowerConfiguration, LocalDataCluster):
-    """Xiaomi power configuration cluster implementation."""
+    """Xiaomi power configuration cluster implementation used for devices that only send battery voltage."""
 
     BATTERY_VOLTAGE_ATTR = PowerConfiguration.AttributeDefs.battery_voltage.id
     BATTERY_PERCENTAGE_REMAINING = (
@@ -501,6 +501,23 @@ class XiaomiPowerConfiguration(PowerConfiguration, LocalDataCluster):
         self._update_attribute(self.BATTERY_PERCENTAGE_REMAINING, percent)
 
 
+class XiaomiPowerConfigurationPercent(XiaomiPowerConfiguration):
+    """Power cluster which ignores Xiaomi voltage reports for calculating battery percentage
+
+    Devices that use this cluster (E1 curtain driver/roller) already send the battery percentage on their own
+    as a separate attribute, but additionally also send the battery voltage.
+    This class only uses the voltage reports for the voltage attribute, but not for the battery percentage.
+    The battery percentage is used as is from the battery percentage reports using inherited battery_percent_reported().
+    """
+
+    def _update_battery_percentage(self, voltage_mv: int) -> None:
+        """Ignore Xiaomi voltage reports, so they're not used to calculate battery percentage."""
+        # This device sends battery percentage reports which are handled using a XiaomiCluster and
+        # the inherited XiaomiPowerConfiguration cluster.
+        # This device might also send Xiaomi battery reports, so we only want to use those for the voltage attribute,
+        # but not for the battery percentage. XiaomiPowerConfiguration.battery_reported() still updates the voltage.
+
+
 class OccupancyCluster(OccupancyWithReset):
     """Occupancy cluster."""
 
@@ -625,6 +642,19 @@ class IlluminanceMeasurementCluster(CustomCluster, IlluminanceMeasurement):
         super()._update_attribute(attrid, value)
 
 
+class LocalIlluminanceMeasurementCluster(
+    LocalDataCluster, IlluminanceMeasurementCluster
+):
+    """Illuminance measurement cluster based on LocalDataCluster."""
+
+    def __init__(self, *args, **kwargs):
+        """Init."""
+        super().__init__(*args, **kwargs)
+        if self.AttributeDefs.measured_value.id not in self._attr_cache:
+            # put a default value so the sensor is created
+            self._update_attribute(self.AttributeDefs.measured_value.id, 0)
+
+
 class OnOffCluster(OnOff, CustomCluster):
     """Aqara wall switch cluster."""
 
diff --git a/zhaquirks/xiaomi/aqara/driver_curtain_e1.py b/zhaquirks/xiaomi/aqara/driver_curtain_e1.py
new file mode 100644
index 0000000..758900a
--- /dev/null
+++ b/zhaquirks/xiaomi/aqara/driver_curtain_e1.py
@@ -0,0 +1,151 @@
+"""Aqara Curtain Driver E1 device."""
+from __future__ import annotations
+
+from typing import Any
+
+from zigpy import types as t
+from zigpy.profiles import zha
+from zigpy.zcl import foundation
+from zigpy.zcl.clusters.closures import WindowCovering
+from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration, Time
+from zigpy.zcl.clusters.measurement import IlluminanceMeasurement
+from zigpy.zdo.types import NodeDescriptor
+
+from zhaquirks import CustomCluster
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    NODE_DESCRIPTOR,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+from zhaquirks.xiaomi import (
+    LUMI,
+    BasicCluster,
+    LocalIlluminanceMeasurementCluster,
+    XiaomiAqaraE1Cluster,
+    XiaomiCustomDevice,
+    XiaomiPowerConfigurationPercent,
+)
+
+HAND_OPEN = 0x0401
+POSITIONS_STORED = 0x0402
+STORE_POSITION = 0x0407
+HOOKS_LOCK = 0x0427
+HOOKS_STATE = 0x0428
+LIGHT_LEVEL = 0x0429
+
+
+class XiaomiAqaraDriverE1(XiaomiAqaraE1Cluster):
+    """Xiaomi Aqara Curtain Driver E1 cluster."""
+
+    attributes = XiaomiAqaraE1Cluster.attributes.copy()
+    attributes.update(
+        {
+            HAND_OPEN: ("hand_open", t.Bool, True),
+            POSITIONS_STORED: ("positions_stored", t.Bool, True),
+            STORE_POSITION: ("store_position", t.uint8_t, True),
+            HOOKS_LOCK: ("hooks_lock", t.uint8_t, True),
+            HOOKS_STATE: ("hooks_state", t.uint8_t, True),
+            LIGHT_LEVEL: ("light_level", t.uint8_t, True),
+        }
+    )
+
+    def _update_attribute(self, attrid, value):
+        if attrid == LIGHT_LEVEL:
+            # Light level value seems like it can be 0, 1, or 2.
+            # Multiply by 50 to map those values to later show: 1 lx, 50 lx, 100 lx.
+            self.endpoint.illuminance.update_attribute(
+                IlluminanceMeasurement.AttributeDefs.measured_value.id,
+                value * 50,
+            )
+        super()._update_attribute(attrid, value)
+
+
+class WindowCoveringE1(CustomCluster, WindowCovering):
+    """Xiaomi Window Covering cluster that maps open/close to lift percentage."""
+
+    async def command(
+        self,
+        command_id: foundation.GeneralCommand | int | t.uint8_t,
+        *args: Any,
+        manufacturer: int | t.uint16_t | None = None,
+        expect_reply: bool = True,
+        tsn: int | t.uint8_t | None = None,
+        **kwargs: Any,
+    ) -> Any:
+        """Overwrite the open/close commands to call the lift percentage command instead."""
+        if command_id == WindowCovering.ServerCommandDefs.up_open.id:
+            command_id = WindowCovering.ServerCommandDefs.go_to_lift_percentage.id
+            args = (0,)
+        elif command_id == WindowCovering.ServerCommandDefs.down_close.id:
+            command_id = WindowCovering.ServerCommandDefs.go_to_lift_percentage.id
+            args = (100,)
+
+        return await super().command(
+            command_id,
+            *args,
+            manufacturer=manufacturer,
+            expect_reply=expect_reply,
+            tsn=tsn,
+            **kwargs,
+        )
+
+
+class DriverE1(XiaomiCustomDevice):
+    """Aqara Curtain Driver E1 device."""
+
+    signature = {
+        MODELS_INFO: [(LUMI, "lumi.curtain.agl001")],
+        ENDPOINTS: {
+            # <SizePrefixedSimpleDescriptor endpoint=1 profile=260 device_type=263
+            # device_version=1
+            # input_clusters=[0, 1, 3, 10, 258, 64704]
+            # output_clusters=[3, 10, 25, 64704]>
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    Time.cluster_id,
+                    WindowCovering.cluster_id,
+                    XiaomiAqaraDriverE1.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                    XiaomiAqaraDriverE1.cluster_id,
+                ],
+            }
+        },
+    }
+    replacement = {
+        NODE_DESCRIPTOR: NodeDescriptor(
+            0x02, 0x40, 0x80, 0x115F, 0x7F, 0x0064, 0x2C00, 0x0064, 0x00
+        ),
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
+                INPUT_CLUSTERS: [
+                    BasicCluster,
+                    XiaomiPowerConfigurationPercent,
+                    Identify.cluster_id,
+                    Time.cluster_id,
+                    WindowCoveringE1,
+                    LocalIlluminanceMeasurementCluster,
+                    XiaomiAqaraDriverE1,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                ],
+            }
+        },
+    }
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..9ef4527
--- /dev/null
+++ b/zhaquirks/xiaomi/aqara/light_acn.py
@@ -0,0 +1,200 @@
+from zigpy import types as t
+from zigpy.profiles import zgp, zha
+from zigpy.zcl.clusters.general import (
+    Alarms,
+    AnalogInput,
+    BinaryInput,
+    DeviceTemperature,
+    GreenPowerProxy,
+    Groups,
+    Identify,
+    LevelControl,
+    MultistateInput,
+    OnOff,
+    Ota,
+    Scenes,
+    Time,
+)
+from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
+from zigpy.zcl.clusters.lighting import Color
+from zigpy.zcl.clusters.smartenergy import Metering
+
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+from zhaquirks.xiaomi import (
+    LUMI,
+    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
+                ],
+            }
+        }
+    }
+
+
+class LumiLightAcn014(XiaomiCustomDevice):
+    """Quirk for Aqara LED Bulb T1 also known as Xiaomi ZNLDP13LM.
+
+    It identifies itself as lumi.light.acn014, however someone reported there are devices with the same model number
+    but identification lumi.light.cwac02.
+    Provides dimmable light control with color temperature setting.
+    This quirk adds support for power on behavior by adding power_outage_memory attribute.
+    """
+
+    signature = {
+        MODELS_INFO: [
+            (LUMI, "lumi.light.acn014"),
+        ],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_TEMPERATURE_LIGHT,
+                INPUT_CLUSTERS: [
+                    BasicCluster.cluster_id,  # 0x0000
+                    DeviceTemperature.cluster_id,  # 0x0002
+                    Identify.cluster_id,  # 0x0003
+                    Groups.cluster_id,  # 0x0004
+                    Scenes.cluster_id,  # 0x0005
+                    OnOff.cluster_id,  # 0x0006
+                    LevelControl.cluster_id,  # 0x0008
+                    Alarms.cluster_id,  # 0x0009
+                    AnalogInput.cluster_id,  # 0x000C
+                    BinaryInput.cluster_id,  # 0x000F
+                    MultistateInput.cluster_id,  # 0x0012
+                    Color.cluster_id,  # 0x0300
+                    Metering.cluster_id,  # 0x0702
+                    ElectricalMeasurement.cluster_id,  # 0x0B04
+                    OppleClusterLight.cluster_id,  # 0xFCC0 - manufacturer specific
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,  # 0x000A
+                    Ota.cluster_id,  # 0x0019
+                ],
+            },
+            21: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_TEMPERATURE_LIGHT,
+                INPUT_CLUSTERS: [
+                    AnalogInput.cluster_id,  # 0x000C
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            31: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_TEMPERATURE_LIGHT,
+                INPUT_CLUSTERS: [
+                    AnalogInput.cluster_id,  # 0x000C
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [
+                    GreenPowerProxy.cluster_id,  # 0x0021
+                ],
+            },
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_TEMPERATURE_LIGHT,
+                INPUT_CLUSTERS: [
+                    BasicCluster,  # 0x0000
+                    DeviceTemperature.cluster_id,  # 0x0002
+                    Identify.cluster_id,  # 0x0003
+                    Groups.cluster_id,  # 0x0004
+                    Scenes.cluster_id,  # 0x0005
+                    OnOff.cluster_id,  # 0x0006
+                    LevelControl.cluster_id,  # 0x0008
+                    Alarms.cluster_id,  # 0x0009
+                    AnalogInput.cluster_id,  # 0x000C
+                    MultistateInput.cluster_id,  # 0x0012
+                    Color.cluster_id,  # 0x0300
+                    OppleClusterLight,  # 0xFCC0 - manufacturer specific
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,  # 0x000A
+                    Ota.cluster_id,  # 0x0019
+                ],
+            },
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [
+                    GreenPowerProxy.cluster_id,  # 0x0021
+                ],
+            },
+        }
+    }
diff --git a/zhaquirks/xiaomi/aqara/magnet_agl02.py b/zhaquirks/xiaomi/aqara/magnet_agl02.py
new file mode 100644
index 0000000..2a63c40
--- /dev/null
+++ b/zhaquirks/xiaomi/aqara/magnet_agl02.py
@@ -0,0 +1,67 @@
+"""Quirk for Aqara T1 door sensor lumi.magnet.agl02."""
+
+from zigpy.profiles import zha
+from zigpy.zcl.clusters.general import Basic, Identify, OnOff, Ota, PowerConfiguration
+from zigpy.zcl.clusters.security import IasZone
+
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+from zhaquirks.xiaomi import (
+    BasicCluster,
+    XiaomiAqaraE1Cluster,
+    XiaomiCustomDevice,
+    XiaomiPowerConfiguration,
+)
+
+
+class MagnetT1(XiaomiCustomDevice):
+    """Aqara T1 door sensor quirk."""
+
+    def __init__(self, *args, **kwargs):
+        """Init."""
+        self.battery_size = 11
+        super().__init__(*args, **kwargs)
+
+    signature = {
+        MODELS_INFO: [("LUMI", "lumi.magnet.agl02")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    IasZone.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Ota.cluster_id,
+                ],
+            },
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
+                INPUT_CLUSTERS: [
+                    BasicCluster,
+                    XiaomiPowerConfiguration,
+                    Identify.cluster_id,
+                    XiaomiAqaraE1Cluster,
+                ],
+                OUTPUT_CLUSTERS: [
+                    OnOff.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+        },
+    }
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 c44f254..a285f2e 100644
--- a/zhaquirks/xiaomi/aqara/motion_ac02.py
+++ b/zhaquirks/xiaomi/aqara/motion_ac02.py
@@ -20,7 +20,7 @@ from zhaquirks.const import (
     PROFILE_ID,
 )
 from zhaquirks.xiaomi import (
-    IlluminanceMeasurementCluster,
+    LocalIlluminanceMeasurementCluster,
     MotionCluster,
     OccupancyCluster,
     XiaomiAqaraE1Cluster,
@@ -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),
@@ -70,17 +69,8 @@ class OppleCluster(XiaomiAqaraE1Cluster):
         return result
 
 
-class LocalIlluminanceMeasurementCluster(
-    LocalDataCluster, IlluminanceMeasurementCluster
-):
-    """Local illuminance measurement cluster."""
-
-    def __init__(self, *args, **kwargs):
-        """Init."""
-        super().__init__(*args, **kwargs)
-        if self.AttributeDefs.measured_value.id not in self._attr_cache:
-            # put a default value so the sensor is created
-            self._update_attribute(self.AttributeDefs.measured_value.id, 0)
+class IlluminanceMeasurementClusterP1(LocalIlluminanceMeasurementCluster):
+    """Local illuminance measurement cluster that also discards more invalid values sent by this device."""
 
     def _update_attribute(self, attrid, value):
         if attrid == self.AttributeDefs.measured_value.id and (
@@ -146,7 +136,7 @@ class LumiMotionAC02(CustomDevice):
                     Identify.cluster_id,
                     LocalOccupancyCluster,
                     LocalMotionCluster,
-                    LocalIlluminanceMeasurementCluster,
+                    IlluminanceMeasurementClusterP1,
                     OppleCluster,
                 ],
                 OUTPUT_CLUSTERS: [
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/roller_curtain_e1.py b/zhaquirks/xiaomi/aqara/roller_curtain_e1.py
index e962feb..8c9e2b1 100644
--- a/zhaquirks/xiaomi/aqara/roller_curtain_e1.py
+++ b/zhaquirks/xiaomi/aqara/roller_curtain_e1.py
@@ -21,7 +21,6 @@ from zigpy.zcl.clusters.general import (
     Scenes,
     Time,
 )
-from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster
 
 from zhaquirks import CustomCluster
 from zhaquirks.const import (
@@ -35,24 +34,16 @@ from zhaquirks.const import (
 from zhaquirks.xiaomi import (
     LUMI,
     BasicCluster,
+    XiaomiAqaraE1Cluster,
     XiaomiCluster,
     XiaomiCustomDevice,
-    XiaomiPowerConfiguration,
+    XiaomiPowerConfigurationPercent,
 )
 
-PRESENT_VALUE = 0x0055
-CURRENT_POSITION_LIFT_PERCENTAGE = 0x0008
-GO_TO_LIFT_PERCENTAGE = 0x0005
-DOWN_CLOSE = 0x0001
-UP_OPEN = 0x0000
-STOP = 0x0002
 
-
-class XiaomiAqaraRollerE1(XiaomiCluster, ManufacturerSpecificCluster):
+class XiaomiAqaraRollerE1(XiaomiAqaraE1Cluster):
     """Xiaomi mfg cluster implementation specific for E1 Roller."""
 
-    cluster_id = 0xFCC0
-
     attributes = XiaomiCluster.attributes.copy()
     attributes.update(
         {
@@ -69,36 +60,28 @@ class XiaomiAqaraRollerE1(XiaomiCluster, ManufacturerSpecificCluster):
 class AnalogOutputRollerE1(CustomCluster, AnalogOutput):
     """Analog output cluster, only used to relay current_value to WindowCovering."""
 
-    cluster_id = AnalogOutput.cluster_id
-
-    def __init__(self, *args: Any, **kwargs: Any) -> None:
-        """Init."""
-        super().__init__(*args, **kwargs)
-
-        self._update_attribute(0x0041, float(0x064))  # max_present_value
-        self._update_attribute(0x0045, 0.0)  # min_present_value
-        self._update_attribute(0x0051, 0)  # out_of_service
-        self._update_attribute(0x006A, 1.0)  # resolution
-        self._update_attribute(0x006F, 0x00)  # status_flags
+    _CONSTANT_ATTRIBUTES = {
+        AnalogOutput.AttributeDefs.description.id: "Current position",
+        AnalogOutput.AttributeDefs.max_present_value.id: 100.0,
+        AnalogOutput.AttributeDefs.min_present_value.id: 0.0,
+        AnalogOutput.AttributeDefs.out_of_service.id: 0,
+        AnalogOutput.AttributeDefs.resolution.id: 1.0,
+        AnalogOutput.AttributeDefs.status_flags.id: 0x00,
+    }
 
     def _update_attribute(self, attrid: int, value: Any) -> None:
         super()._update_attribute(attrid, value)
 
-        if attrid == PRESENT_VALUE:
-            self.endpoint.window_covering._update_attribute(
-                CURRENT_POSITION_LIFT_PERCENTAGE, (100 - value)
+        if attrid == self.AttributeDefs.present_value.id:
+            self.endpoint.window_covering.update_attribute(
+                WindowCovering.AttributeDefs.current_position_lift_percentage.id,
+                (100 - value),
             )
 
 
 class WindowCoveringRollerE1(CustomCluster, WindowCovering):
     """Window covering cluster to receive commands that are sent to the AnalogOutput's present_value to move the motor."""
 
-    cluster_id = WindowCovering.cluster_id
-
-    def __init__(self, *args: Any, **kwargs: Any) -> None:
-        """Init."""
-        super().__init__(*args, **kwargs)
-
     async def command(
         self,
         command_id: foundation.GeneralCommand | int | t.uint8_t,
@@ -113,28 +96,28 @@ class WindowCoveringRollerE1(CustomCluster, WindowCovering):
         We either overwrite analog_output's current_value or multistate_output's current
         value to make the roller work.
         """
-        if command_id == UP_OPEN:
+        if command_id == WindowCovering.ServerCommandDefs.up_open.id:
             (res,) = await self.endpoint.multistate_output.write_attributes(
                 {"present_value": 1}
             )
             return foundation.GENERAL_COMMANDS[
                 foundation.GeneralCommand.Default_Response
             ].schema(command_id=command_id, status=res[0].status)
-        if command_id == DOWN_CLOSE:
+        if command_id == WindowCovering.ServerCommandDefs.down_close.id:
             (res,) = await self.endpoint.multistate_output.write_attributes(
                 {"present_value": 0}
             )
             return foundation.GENERAL_COMMANDS[
                 foundation.GeneralCommand.Default_Response
             ].schema(command_id=command_id, status=res[0].status)
-        if command_id == GO_TO_LIFT_PERCENTAGE:
+        if command_id == WindowCovering.ServerCommandDefs.go_to_lift_percentage.id:
             (res,) = await self.endpoint.analog_output.write_attributes(
                 {"present_value": (100 - args[0])}
             )
             return foundation.GENERAL_COMMANDS[
                 foundation.GeneralCommand.Default_Response
             ].schema(command_id=command_id, status=res[0].status)
-        if command_id == STOP:
+        if command_id == WindowCovering.ServerCommandDefs.stop.id:
             (res,) = await self.endpoint.multistate_output.write_attributes(
                 {"present_value": 2}
             )
@@ -152,22 +135,14 @@ class MultistateOutputRollerE1(CustomCluster, MultistateOutput):
     attributes = MultistateOutput.attributes.copy()
     attributes.update(
         {
-            0x0055: ("present_value", t.uint16_t),
+            MultistateOutput.AttributeDefs.present_value.id: (
+                "present_value",
+                t.uint16_t,
+            ),
         }
     )
 
 
-class PowerConfigurationRollerE1(XiaomiPowerConfiguration):
-    """Power cluster which ignores Xiaomi voltage reports."""
-
-    def _update_battery_percentage(self, voltage_mv: int) -> None:
-        """Ignore Xiaomi voltage reports, so they're not used to calculate battery percentage."""
-        # This device sends battery percentage reports which are handled using a XiaomiCluster and
-        # the inherited XiaomiPowerConfiguration cluster.
-        # This device might also send Xiaomi battery reports, so we only want to use those for the voltage attribute,
-        # but not for the battery percentage. XiaomiPowerConfiguration.battery_reported() still updates the voltage.
-
-
 class RollerE1AQ(XiaomiCustomDevice):
     """Aqara Roller Shade Driver E1 device."""
 
@@ -229,7 +204,7 @@ class RollerE1AQ(XiaomiCustomDevice):
                     MultistateOutputRollerE1,
                     Scenes.cluster_id,
                     WindowCoveringRollerE1,
-                    PowerConfigurationRollerE1,
+                    XiaomiPowerConfigurationPercent,
                 ],
                 OUTPUT_CLUSTERS: [
                     Ota.cluster_id,
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),
     }
diff --git a/zhaquirks/xiaomi/aqara/vibration_aq1.py b/zhaquirks/xiaomi/aqara/vibration_aq1.py
index b46a859..fb1dcbf 100644
--- a/zhaquirks/xiaomi/aqara/vibration_aq1.py
+++ b/zhaquirks/xiaomi/aqara/vibration_aq1.py
@@ -35,6 +35,7 @@ from zhaquirks.const import (
     ZHA_SEND_EVENT,
     ZONE_TYPE,
 )
+from zhaquirks.quirk_ids import XIAOMI_AQARA_VIBRATION_AQ1
 from zhaquirks.xiaomi import (
     LUMI,
     XIAOMI_NODE_DESC,
@@ -69,6 +70,8 @@ _LOGGER = logging.getLogger(__name__)
 class VibrationAQ1(XiaomiQuickInitDevice):
     """Xiaomi aqara smart motion sensor device."""
 
+    quirk_id = XIAOMI_AQARA_VIBRATION_AQ1
+
     manufacturer_id_override = 0x115F
 
     def __init__(self, *args, **kwargs):


Test patch:
diff --git a/tests/test_xbee.py b/tests/test_xbee.py
index 24b21e3..91b986e 100644
--- a/tests/test_xbee.py
+++ b/tests/test_xbee.py
@@ -641,3 +641,21 @@ async def test_io_sample_report_on_at_response(zigpy_device_from_quirk):
     assert 33.33333 < analog_listeners[0].attribute_updates[0][1] < 33.33334
     assert 66.66666 < analog_listeners[2].attribute_updates[0][1] < 66.66667
     assert analog_listeners[4].attribute_updates[0] == (0x0055, 3.305)
+
+
+@mock.patch("zigpy.zdo.ZDO.handle_ieee_addr_req")
+async def test_zdo(handle_mgmt_lqi_resp, zigpy_device_from_quirk):
+    """Test receiving ZDO data from XBee device."""
+
+    xbee3_device = zigpy_device_from_quirk(XBee3Sensor)
+
+    # Receive ZDOCmd.IEEE_addr_req
+    xbee3_device.handle_message(0, 0x0001, 0, 0, b"\x07\x34\x12\x00\x00")
+
+    assert handle_mgmt_lqi_resp.call_count == 1
+    assert len(handle_mgmt_lqi_resp.call_args_list[0][0]) == 4
+    assert handle_mgmt_lqi_resp.call_args_list[0][0][0].tsn == 0x07
+    assert handle_mgmt_lqi_resp.call_args_list[0][0][0].command_id == 0x0001
+    assert handle_mgmt_lqi_resp.call_args_list[0][0][1] == 0x1234
+    assert handle_mgmt_lqi_resp.call_args_list[0][0][2] == 0
+    assert handle_mgmt_lqi_resp.call_args_list[0][0][3] == 0
diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py
index 624f19e..6122b67 100644
--- a/tests/test_xiaomi.py
+++ b/tests/test_xiaomi.py
@@ -8,9 +8,13 @@ import pytest
 import zigpy.device
 import zigpy.types as t
 from zigpy.zcl import foundation
+from zigpy.zcl.clusters.closures import WindowCovering
 from zigpy.zcl.clusters.general import (
     AnalogInput,
+    AnalogOutput,
     DeviceTemperature,
+    MultistateOutput,
+    OnOff,
     PowerConfiguration,
 )
 from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
@@ -49,6 +53,7 @@ from zhaquirks.xiaomi import (
     XiaomiQuickInitDevice,
     handle_quick_init,
 )
+import zhaquirks.xiaomi.aqara.driver_curtain_e1
 from zhaquirks.xiaomi.aqara.feeder_acn001 import (
     FEEDER_ATTR,
     ZCL_CHILD_LOCK,
@@ -65,6 +70,7 @@ from zhaquirks.xiaomi.aqara.feeder_acn001 import (
     AqaraFeederAcn001,
     OppleCluster,
 )
+import zhaquirks.xiaomi.aqara.magnet_agl02
 import zhaquirks.xiaomi.aqara.motion_ac02
 import zhaquirks.xiaomi.aqara.motion_aq2
 import zhaquirks.xiaomi.aqara.motion_aq2b
@@ -1283,3 +1289,308 @@ async def test_xiaomi_e1_roller_curtain_battery(zigpy_device_from_quirk, quirk):
     assert power_listener.attribute_updates[0][1] == 28.9
     assert power_listener.attribute_updates[1][0] == zcl_power_percent_id
     assert power_listener.attribute_updates[1][1] == 120
+
+
+@pytest.mark.parametrize(
+    "raw_report, expected_results",
+    (
+        [
+            "1C5F11670AF700412E0121B00C0328190421A8130521090006240D0000000008211E010A2100000C20016410016620036720016821A800",
+            [
+                32.5,  # battery voltage
+                200,  # battery percent * 2
+            ],
+        ],
+    ),
+)
+async def test_xiaomi_t1_door_sensor(
+    zigpy_device_from_quirk, raw_report, expected_results
+):
+    """Test Aqara T1 door sensor."""
+    raw_report = bytes.fromhex(raw_report)
+
+    device = zigpy_device_from_quirk(zhaquirks.xiaomi.aqara.magnet_agl02.MagnetT1)
+
+    on_off_cluster = device.endpoints[1].out_clusters[OnOff.cluster_id]
+    on_off_listener = ClusterListener(on_off_cluster)
+
+    # check open state
+    device.handle_message(
+        260,
+        on_off_cluster.cluster_id,
+        on_off_cluster.endpoint.endpoint_id,
+        on_off_cluster.endpoint.endpoint_id,
+        bytes.fromhex("185D0A00001001"),
+    )
+
+    assert len(on_off_listener.attribute_updates) == 1
+    assert on_off_listener.attribute_updates[0][0] == OnOff.AttributeDefs.on_off.id
+    assert on_off_listener.attribute_updates[0][1] == t.Bool.true
+
+    # check closed state
+    device.handle_message(
+        260,
+        on_off_cluster.cluster_id,
+        on_off_cluster.endpoint.endpoint_id,
+        on_off_cluster.endpoint.endpoint_id,
+        bytes.fromhex("18640A00001000"),
+    )
+
+    assert len(on_off_listener.attribute_updates) == 2
+    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].opple_cluster
+    ClusterListener(opple_cluster)
+
+    power_cluster = device.endpoints[1].power
+    power_listener = ClusterListener(power_cluster)
+
+    zcl_power_voltage_id = PowerConfiguration.AttributeDefs.battery_voltage.id
+    zcl_power_percent_id = (
+        PowerConfiguration.AttributeDefs.battery_percentage_remaining.id
+    )
+
+    # check Xiaomi attribute report
+    device.handle_message(
+        260,
+        opple_cluster.cluster_id,
+        opple_cluster.endpoint.endpoint_id,
+        opple_cluster.endpoint.endpoint_id,
+        raw_report,
+    )
+
+    assert len(power_listener.attribute_updates) == 2
+    assert power_listener.attribute_updates[0][0] == zcl_power_voltage_id
+    assert power_listener.attribute_updates[0][1] == expected_results[0]
+    assert power_listener.attribute_updates[1][0] == zcl_power_percent_id
+    assert power_listener.attribute_updates[1][1] == expected_results[1]
+
+
+@pytest.mark.parametrize(
+    "command, command_id, value",
+    [
+        (
+            WindowCovering.ServerCommandDefs.up_open.id,
+            WindowCovering.ServerCommandDefs.go_to_lift_percentage.id,
+            0,
+        ),
+        (
+            WindowCovering.ServerCommandDefs.down_close.id,
+            WindowCovering.ServerCommandDefs.go_to_lift_percentage.id,
+            100,
+        ),
+        (
+            WindowCovering.ServerCommandDefs.stop.id,
+            WindowCovering.ServerCommandDefs.stop.id,
+            None,
+        ),
+    ],
+)
+async def test_xiaomi_e1_driver_commands(
+    zigpy_device_from_quirk, command, command_id, value
+):
+    """Test Aqara E1 driver commands for basic movement functions using WindowCovering cluster."""
+    device = zigpy_device_from_quirk(zhaquirks.xiaomi.aqara.driver_curtain_e1.DriverE1)
+
+    window_covering_cluster = device.endpoints[1].window_covering
+    p = mock.patch.object(window_covering_cluster, "request", mock.AsyncMock())
+
+    with p as request_mock:
+        request_mock.return_value = (foundation.Status.SUCCESS, "done")
+
+        # test command
+        await window_covering_cluster.command(command)
+        assert request_mock.call_count == 1
+        assert request_mock.call_args[0][1] == command_id
+        if value is not None:
+            assert request_mock.call_args[0][3] == value
+
+
+@pytest.mark.parametrize(
+    "device_level, converted_level",
+    [
+        (0, 0),
+        (1, 50),
+        (2, 100),
+    ],
+)
+async def test_xiaomi_e1_driver_light_level(
+    zigpy_device_from_quirk, device_level, converted_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].opple_cluster
+    opple_listener = ClusterListener(opple_cluster)
+    opple_zcl_iilluminance_id = 0x0429
+
+    illuminance_cluster = device.endpoints[1].illuminance
+    illuminance_listener = ClusterListener(illuminance_cluster)
+    zcl_iilluminance_id = IlluminanceMeasurement.AttributeDefs.measured_value.id
+
+    # send motion and illuminance report 10
+    opple_cluster.update_attribute(opple_zcl_iilluminance_id, device_level)
+
+    # confirm manufacturer specific attribute report
+    assert len(opple_listener.attribute_updates) == 1
+    assert opple_listener.attribute_updates[0][0] == opple_zcl_iilluminance_id
+    assert opple_listener.attribute_updates[0][1] == device_level
+
+    # confirm illuminance report (with conversion)
+    assert len(illuminance_listener.attribute_updates) == 1
+    assert illuminance_listener.attribute_updates[0][0] == zcl_iilluminance_id
+
+    assert (
+        device_level == 0
+        and converted_level == 0
+        or (
+            illuminance_listener.attribute_updates[0][1]
+            == 10000 * math.log10(converted_level) + 1
+        )
+    )
+
+
+@pytest.mark.parametrize(
+    "command, value",
+    [
+        (WindowCovering.ServerCommandDefs.up_open.id, 1),
+        (WindowCovering.ServerCommandDefs.down_close.id, 0),
+        (WindowCovering.ServerCommandDefs.stop.id, 2),
+    ],
+)
+async def test_xiaomi_e1_roller_commands_1(zigpy_device_from_quirk, command, value):
+    """Test Aqara E1 roller commands for basic movement functions using MultistateOutput Cluster."""
+    device = zigpy_device_from_quirk(
+        zhaquirks.xiaomi.aqara.roller_curtain_e1.RollerE1AQ
+    )
+
+    window_covering_cluster = device.endpoints[1].window_covering
+    multistate_cluster = device.endpoints[1].multistate_output
+    multistate_cluster._write_attributes = mock.AsyncMock(
+        return_value=(
+            [foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)],
+        )
+    )
+    attr_id = MultistateOutput.AttributeDefs.present_value.id
+
+    # test command
+    await window_covering_cluster.command(command)
+    assert multistate_cluster._write_attributes.call_count == 1
+    assert multistate_cluster._write_attributes.call_args[0][0][0].attrid == attr_id
+    assert multistate_cluster._write_attributes.call_args[0][0][0].value.value == value
+
+
+@pytest.mark.parametrize(
+    "command, value",
+    [
+        (WindowCovering.ServerCommandDefs.go_to_lift_percentage.id, 60),
+    ],
+)
+async def test_xiaomi_e1_roller_commands_2(zigpy_device_from_quirk, command, value):
+    """Test Aqara E1 roller commands for go to lift percentage using AnalogOutput cluster."""
+    device = zigpy_device_from_quirk(
+        zhaquirks.xiaomi.aqara.roller_curtain_e1.RollerE1AQ
+    )
+
+    window_covering_cluster = device.endpoints[1].window_covering
+    analog_cluster = device.endpoints[1].analog_output
+    analog_cluster._write_attributes = mock.AsyncMock(
+        return_value=(
+            [foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)],
+        )
+    )
+    attr_id = AnalogOutput.AttributeDefs.present_value.id
+
+    # test go to lift percentage command
+    await window_covering_cluster.go_to_lift_percentage(value)
+    assert analog_cluster._write_attributes.call_count == 1
+    assert analog_cluster._write_attributes.call_args[0][0][0].attrid == attr_id
+    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
+    )
+
+
+def test_aqara_acn014_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": "0x010c",
+                "in_clusters": [
+                    "0x0000",
+                    "0x0002",
+                    "0x0003",
+                    "0x0004",
+                    "0x0005",
+                    "0x0006",
+                    "0x0008",
+                    "0x0009",
+                    "0x000c",
+                    "0x000f",
+                    "0x0012",
+                    "0x0300",
+                    "0x0702",
+                    "0x0b04",
+                    "0xfcc0",
+                ],
+                "out_clusters": ["0x000a", "0x0019"],
+            },
+            "21": {
+                "profile_id": 0x0104,
+                "device_type": "0x010c",
+                "in_clusters": ["0x000c"],
+                "out_clusters": [],
+            },
+            "31": {
+                "profile_id": 0x0104,
+                "device_type": "0x010c",
+                "in_clusters": ["0x000c"],
+                "out_clusters": [],
+            },
+            "242": {
+                "profile_id": 0xA1E0,
+                "device_type": "0x0061",
+                "in_clusters": [],
+                "out_clusters": ["0x0021"],
+            },
+        },
+        "manufacturer": "LUMI",
+        "model": "lumi.light.acn014",
+        "class": "zigpy.device.Device",
+    }
+
+    assert_signature_matches_quirk(
+        zhaquirks.xiaomi.aqara.light_acn.LumiLightAcn014, signature
+    )