Repository: zigpy/zha-device-handlers

Issue:
[Device Support Request] Aqara Curtain Driver E1 (lumi.curtain.agl001)
**Is your feature request related to a problem? Please describe.**
Am able to open and close curtains using the controls in HA, however have to physically turn the device around to change opening direction. The device supports reversing direction, but an entity for this does not appear in ZHA, neither does the battery level. Power source is incorrectly listed as mains powered and device firmware version is not listed.
Lastly, the device has a light level sensor but again no entity for this in ZHA. 

**Describe the solution you'd like**

- Entity to reverse opening direction.
- Battery level sensor.
- Light level sensor.
- Correct Power source to battery.
- Display device firmware version.

<details>
<!-- Device signature can be acquired by clicking on the "Zigbee Device Signature" button in the device settings view -->
<summary>Device signature</summary>

```yaml
{
  "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|MainsPowered: 132>, manufacturer_code=4447, maximum_buffer_size=127, maximum_incoming_transfer_size=100, server_mask=11264, maximum_outgoing_transfer_size=100, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=True, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": 260,
      "device_type": "0x0107",
      "in_clusters": [
        "0x0000",
        "0x0001",
        "0x0003",
        "0x000a",
        "0x0102",
        "0xfcc0"
      ],
      "out_clusters": [
        "0x0003",
        "0x000a",
        "0x0019",
        "0xfcc0"
      ]
    }
  },
  "manufacturer": "LUMI",
  "model": "lumi.curtain.agl001",
  "class": "zigpy.device.Device"
}
```

</details>

<details>
<!-- Diagnostic information can be acquired by clicking on the "Download Diagnostics" button in the device settings view -->
<summary>Diagnostic information</summary>

```yaml
{
  "home_assistant": {
    "installation_type": "Home Assistant OS",
    "version": "2022.12.1",
    "dev": false,
    "hassio": true,
    "virtualenv": false,
    "python_version": "3.10.7",
    "docker": true,
    "arch": "aarch64",
    "timezone": "Europe/London",
    "os_name": "Linux",
    "os_version": "5.15.74",
    "supervisor": "2022.11.2",
    "host_os": "Home Assistant OS 9.3",
    "docker_version": "20.10.18",
    "chassis": "embedded",
    "run_as_root": true
  },
  "custom_components": {
    "hacs": {
      "version": "1.28.4",
      "requirements": [
        "aiogithubapi>=22.2.4"
      ]
    },
    "dukaone": {
      "version": "1.0.6",
      "requirements": [
        "dukaonesdk==1.0.4"
      ]
    },
    "nuki_ng": {
      "version": "0.2.1",
      "requirements": []
    },
    "feedparser": {
      "version": "0.1.7",
      "requirements": [
        "feedparser==6.0.8"
      ]
    },
    "browser_mod": {
      "version": "2.1.2",
      "requirements": []
    },
    "wiser": {
      "version": "3.2.2",
      "requirements": [
        "aioWiserHeatAPI==0.1.8"
      ]
    }
  },
  "integration_manifest": {
    "domain": "zha",
    "name": "Zigbee Home Automation",
    "config_flow": true,
    "documentation": "https://www.home-assistant.io/integrations/zha",
    "requirements": [
      "bellows==0.34.5",
      "pyserial==3.5",
      "pyserial-asyncio==0.6",
      "zha-quirks==0.0.88",
      "zigpy-deconz==0.19.2",
      "zigpy==0.52.3",
      "zigpy-xbee==0.16.2",
      "zigpy-zigate==0.10.3",
      "zigpy-znp==0.9.2"
    ],
    "usb": [
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*2652*",
        "known_devices": [
          "slae.sh cc2652rb stick"
        ]
      },
      {
        "vid": "1A86",
        "pid": "55D4",
        "description": "*sonoff*plus*",
        "known_devices": [
          "sonoff zigbee dongle plus v2"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*sonoff*plus*",
        "known_devices": [
          "sonoff zigbee dongle plus"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*tubeszb*",
        "known_devices": [
          "TubesZB Coordinator"
        ]
      },
      {
        "vid": "1A86",
        "pid": "7523",
        "description": "*tubeszb*",
        "known_devices": [
          "TubesZB Coordinator"
        ]
      },
      {
        "vid": "1A86",
        "pid": "7523",
        "description": "*zigstar*",
        "known_devices": [
          "ZigStar Coordinators"
        ]
      },
      {
        "vid": "1CF1",
        "pid": "0030",
        "description": "*conbee*",
        "known_devices": [
          "Conbee II"
        ]
      },
      {
        "vid": "10C4",
        "pid": "8A2A",
        "description": "*zigbee*",
        "known_devices": [
          "Nortek HUSBZB-1"
        ]
      },
      {
        "vid": "0403",
        "pid": "6015",
        "description": "*zigate*",
        "known_devices": [
          "ZiGate+"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*zigate*",
        "known_devices": [
          "ZiGate"
        ]
      },
      {
        "vid": "10C4",
        "pid": "8B34",
        "description": "*bv 2010/10*",
        "known_devices": [
          "Bitron Video AV2010/10"
        ]
      }
    ],
    "codeowners": [
      "@dmulcahey",
      "@adminiuga",
      "@puddly"
    ],
    "zeroconf": [
      {
        "type": "_esphomelib._tcp.local.",
        "name": "tube*"
      },
      {
        "type": "_zigate-zigbee-gateway._tcp.local.",
        "name": "*zigate*"
      },
      {
        "type": "_zigstar_gw._tcp.local.",
        "name": "*zigstar*"
      }
    ],
    "dependencies": [
      "file_upload"
    ],
    "after_dependencies": [
      "onboarding",
      "usb",
      "zeroconf"
    ],
    "iot_class": "local_polling",
    "loggers": [
      "aiosqlite",
      "bellows",
      "crccheck",
      "pure_pcapy3",
      "zhaquirks",
      "zigpy",
      "zigpy_deconz",
      "zigpy_xbee",
      "zigpy_zigate",
      "zigpy_znp"
    ],
    "is_built_in": true
  },
  "data": {
    "ieee": "**REDACTED**",
    "nwk": 25329,
    "manufacturer": "LUMI",
    "model": "lumi.curtain.agl001",
    "name": "LUMI lumi.curtain.agl001",
    "quirk_applied": false,
    "quirk_class": "zigpy.device.Device",
    "manufacturer_code": 4447,
    "power_source": "Mains",
    "lqi": 83,
    "rssi": null,
    "last_seen": "2022-12-09T23:34:27",
    "available": true,
    "device_type": "EndDevice",
    "signature": {
      "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|MainsPowered: 132>, manufacturer_code=4447, maximum_buffer_size=127, maximum_incoming_transfer_size=100, server_mask=11264, maximum_outgoing_transfer_size=100, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=True, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
      "endpoints": {
        "1": {
          "profile_id": 260,
          "device_type": "0x0107",
          "in_clusters": [
            "0x0000",
            "0x0001",
            "0x0003",
            "0x000a",
            "0x0102",
            "0xfcc0"
          ],
          "out_clusters": [
            "0x0003",
            "0x000a",
            "0x0019",
            "0xfcc0"
          ]
        }
      }
    },
    "active_coordinator": false,
    "entities": [
      {
        "entity_id": "cover.bedroom_curtain",
        "name": "LUMI lumi.curtain.agl001"
      },
      {
        "entity_id": "button.bedroom_curtain_identify",
        "name": "LUMI lumi.curtain.agl001"
      }
    ],
    "neighbors": [],
    "routes": [],
    "endpoint_names": [
      {
        "name": "OCCUPANCY_SENSOR"
      }
    ],
    "user_given_name": "Bedroom curtain",
    "device_reg_id": "df6da383942e4fd7ba629c542d54d4c3",
    "area_id": "cee9dcbe51864ffb940f792c08941a77",
    "cluster_details": {
      "1": {
        "device_type": {
          "name": "OCCUPANCY_SENSOR",
          "id": 263
        },
        "profile_id": 260,
        "in_clusters": {
          "0x0000": {
            "endpoint_attribute": "basic",
            "attributes": {
              "0x0004": {
                "attribute_name": "manufacturer",
                "value": "LUMI"
              },
              "0x0005": {
                "attribute_name": "model",
                "value": "lumi.curtain.agl001"
              }
            },
            "unsupported_attributes": {}
          },
          "0x0001": {
            "endpoint_attribute": "power",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0003": {
            "endpoint_attribute": "identify",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0xfcc0": {
            "endpoint_attribute": "manufacturer_specific",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0102": {
            "endpoint_attribute": "window_covering",
            "attributes": {
              "0x0008": {
                "attribute_name": "current_position_lift_percentage",
                "value": 0
              }
            },
            "unsupported_attributes": {}
          },
          "0x000a": {
            "endpoint_attribute": "time",
            "attributes": {},
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {
          "0x0003": {
            "endpoint_attribute": "identify",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0019": {
            "endpoint_attribute": "ota",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0xfcc0": {
            "endpoint_attribute": "manufacturer_specific",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x000a": {
            "endpoint_attribute": "time",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      }
    }
  }
}
```

</details>

<details>
<summary>Additional logs</summary>

```
Paste any additional debug logs here.
Don't remove the extra line breaks outside the ``` marks.
```

</details>

**Additional context**
https://zigbee.blakadder.com/Aqara_ZNCLBL01LM.html

There's a pr for this device [here](https://github.com/zigpy/zha-device-handlers/pull/1648), but it would appear progress has stalled. This currently adds support for the battery level sensor, but the reversing direction and light level sensor don’t work.



Discussion:
I have one of this for testing. The #1648 mentioned above works well to add battery level and direction control option. However there's something strange about this device. When I set `Curtain mode` to `Inverted` everything works fine, but when I set it to normal, the close/open directions seem to be swapped. For example, I if set position to 5 and send down_close command I'm expecting the device to move towards the side it is close to. However the device moves towards the farther edge and reports position 100. Once there the close button in UI is no longer available and open button does not work.

As I understand this device also has a luminosity sensor, would be nice to have one available in Home assistant or to set rules for automatic opening/closing.
For me, using the custom quirk only adds battery level, the direction control option doesn't show up for me. I tried updating the Aqara E1 Curtain driver to the latest firmware using an Aqara hub, but no joy.
@circa1665 Did you check the clusters UI (with the custom quirk applied)?

Go to the device page -> click the three dots -> Manage Zigbee Device. In the first dropdown menu, select ``XiaomiAqaraCurtainE1`` (if it shows up) and then you can modify the attributes.
Note: always leave the second text field empty. When you're reading an attribute, it'll appear in the first text field. If you want to write that attribute, just remove the text from the first text field and enter whatever you want to write in the first text field. (second text field needs to be empty), then press "Write Zigbee attribute."
Hi @TheJulianJES, thanks for the tip, I was able to reverse opening direction by reading attribute value of `XiaomiAqaraCurtainE1 (Endpoint id: 1, Id: 0xfcc0, Type: in)` > `store_position (id: 0x0407)` and restarting.

Unrelated, have noticed that battery state is now permanently "Unknown".
@circa1665 Can you restart HA now and see if you get the config entity?
Not sure why the battery entity isn't working though, maybe it comes back after some time
@TheJulianJES, yes! The curtain mode entity is showing up now - although it's showing as "Normal", rather than "Inverted".
<img width="330" alt="Screenshot 2023-01-04 at 22 39 21" src="https://user-images.githubusercontent.com/33236082/210664025-e3dd9233-c448-4785-8112-14d9e0074945.png">
Re: The battery, not sure when that changed but it's definitely been several days. Have tried removing and re-adding the device to ZHA several times.

Changing the curtain mode to "inverted" (whilst the device is awakened) should properly update the status there.

https://github.com/home-assistant/core/pull/75132 did not add the attribute to ``ZCL_INIT_ARGS`` for the [``WindowCovering`` channel here](https://github.com/home-assistant/core/blob/80c357c00f733a90534fc9999fe5b606849c3b16/homeassistant/components/zha/core/channels/closures.py#L120-L122).

- it needs to be added to init args, so the attribute is read during pairing (and thus the config entity created)
- ``"window_covering"`` from that PR should also be changed to the constant ``CHANNEL_COVER``
- [the quirk](https://github.com/zigpy/zha-device-handlers/pull/1648) also needs to be merged into zha-quirks
Just to flag, the battery state is still "Unknown" so not a case of it coming back "after some time", thanks!
What coordinator are you using?
I'm using  a SONOFF Zigbee 3.0 USB Dongle Plus with this firmware: https://github.com/Koenkk/Z-Stack-firmware
I am trying to control a curtain set with two lumi.curtain.agl001 motors. Not so easy.  

Just as mentioned above I am missing
-    Entity to reverse opening direction.
-    Battery level sensor.
-    Light level sensor.
-    Correct Power source to battery.
-    Display device firmware version.

I can work around the reverse directions for the right curtain, but not around the other issues.

But I have another major problem ..... that is the position of the motors. There is a command ^cover.set_cover_position^ and according to info found position has a range from 0 .. 100 where  0 = closed and 100 is open,  (https://www.home-assistant.io/integrations/cover.template/)

However that is where the problems are starting:
- because "how to set the open position" and "how to set the closed position"
- and for the right side curtain it is of cause mirrored
- and you have to re-calibrate the "open position" and the "closed position" every time the roller slipped 

I have really no idea how to set those closed and open position reference points using the software
And the behavoir when changing the position slider .... is strange .... I do not understand

What ever I would be **very very very glad** with an updated ZHA-driver and perhaps also firmware for this device !!!

To work around the positioning issues, I consider to use sensors at the closed and open position at each site ..... 
That makes the whole thing of course more expensive and far more complex .... but I do not see an other option at the moment.  .... And it does not help against other issues like ..... unkown battery level etc ...

So, again updates will be highly appreciated 






  



 



  



Have updated SONOFF Zigbee 3.0 USB Dongle Plus to latest firmware: https://github.com/Koenkk/Z-Stack-firmware/blob/Z-Stack_3.x.0_coordinator_20221226/coordinator/Z-Stack_3.x.0/bin/CC1352P2_CC2652P_launchpad_coordinator_20221226.zip
Hasn't made any difference, battery state is still "Unknown".
So from the original list, looks like:
- ~~Entity to reverse opening direction.~~ - sorted but yet to be added to custom quirk (current workaround [here](https://github.com/zigpy/zha-device-handlers/issues/2003#issuecomment-1371503590)).
- Battery level sensor.
- Light level sensor.
- Correct Power source to battery.
- Display device firmware version.

Would be great to get the remaining points working, is there anything I can do to help?
Actuality  I am using the latest Home Assistant version 2023.2.2 version and probably more relevant the recent Sonoff E USB (EZSP = Silicon Labs EmberZNet protocol: Elelabs, HUSBZB-1, Telegesis by ZHA). Before I used the older Sonoff P USB.

Since I made that move, the position slider and the related the  set_cover_position command is gone. 

Since I used to open and close the curtains with the cover_position command (which was more reliable then the open/close command) from the gui and even more relevant from node-red, two points:

- I think the now missing go to position -x should be added to the to do list
- I hope that someone knows how to get the slider back !!

I added my Aqara E1 curtain driver to Home Assistant Bridge/HomeKit and now the battery sensor works.

I've noticed that the state of the entity gets stuck on opening/closing so I've had to create an automation to listen for the events `cover.open_cover` and `cover.close_cover`, then `cover.stop_cover` after a 20 second delay - would have used the `current_position` attribute of the entity, but this only seems to update sometimes and not consistently.

I've also noticed that when I interact with the curtain (`cover.open_cover`, `cover.close_cover`, `cover.stop_cover`) that nothing shows up at all when I listen for `zha_event` in Developer Tools. There doesn't seem to be a `zha_event` when the curtain driver reaches either end of its range.

Lastly this doesn't seem to play nice with Home Assistant Bridge/HomeKit, can't operate this device from HomeKit.
> I added my Aqara E1 curtain driver to Home Assistant Bridge/HomeKit and now the battery sensor works.


How do you add it to HomeKit and why does the battery work with this method? I thought HomeKit didn't support Zigbee without a Zigbee bridge?
@Hamish1066, I don’t know why, the battery sensor just started working when I did. I use the [HomeKit integration](https://www.home-assistant.io/integrations/homekit/) in Home Assistant to make some devices available to control via Homekit. 
@circa1665 I'm also using the HomeKit integration but battery status isn't showing up in HA for me. Weird. 
Recently migrated to Home Assistant Yellow, now the Aqara Curtain Driver E1 does not work with ZHA. This is the only zigbee device I've had any trouble with.

Wondering if this might be because Home Assistant Yellow radio type is EZSP, whereas my previous coordinator was a Sonoff ZBDongle-P CC2652P (Texas Instruments based)? Sounds like [LouisAtGH](https://github.com/LouisAtGH) was describing something similar.

I can get the device to pair if I try repeatedly. But cannot control the device at all. I can't read the reading attribute value of `XiaomiAqaraCurtainE1 (Endpoint id: 1, Id: 0xfcc0, Type: in)` > `store_position (id: 0x0407)` as I was able to previously.
And the status of all the device's entities is `Unknown`.
Much as I would love to get this working with ZHA, doesn’t appear that’s going to happening anytime soon.

Ended up purchasing an [Aqara E1 hub USB dongle](https://www.amazon.co.uk/Aqara-Interface-Repeater-Supports-Assistant/dp/B09BJCNCKM). Now have this working via HomeKit controller in Home Assistant.

The reversing direction isn't exposed as an entity in HA and has to be setup via the Aqara Home app. I now have a functioning cover entity and battery level in Home Assistant which will have to do for now.
Tja I undestand, but for me that is the wrong direction. 

HA should be in control, not the suppliers cloud! 
I would like to stay away from all kind of cloud services, especially chinese ones!

I have the curtains working, using node-red and sensors at the begin and end of each curtain side. 
I is not ideal, but with limitations it works  
I appreciate all the work that @circa1665 has done on this, but I also agree with @LouisAtGH that the goal should be local integration. 

For me, almost everything works using the device with ZHA. The only quirks are that (1) the battery sensor doesn't show up; and (2) I can't use the open/close function, but it works instead to set a position (i.e. 0-100%). So for me it is totally usable with ZHA, just not perfect. 
I have four of these.  Two on two windows.  I just applied the custom quirk and battery level is properly exposed on all four units.

I'm observing similar behaviours as others with regards to their state.  State, current_position and sending open or close commands through the GUI in HomeAssistant are very inconsistent.

Sending open or close commands through Home Assistant service calls works 100% of the time.

EDIT:  that said, even though they move in the correct direction for open and close, when they _do_ report their current_position_lift_percentage it is inverted relative to what Home Assitant is expecting (100 is open, 0 is closed).  The custom quirk inverts the position value if the direction is inverted but, at least with my four, the position value should be inverted in the normal direction instead.
> I just applied the custom quirk and battery level is properly exposed on all four units.

Could you please explain what the custom quirk is. Sorry if I’m missing something. 


> > I just applied the custom quirk and battery level is properly exposed on all four units.
> 
> Could you please explain what the custom quirk is. Sorry if I’m missing something.

It is the PR mentioned in the original post:
https://github.com/zigpy/zha-device-handlers/pull/1648
Is there any testing that needs to be done? I've just received a new one of these and was hoping it would be easily integrated in to HA. The existing quick PR also has an unknown battery for me and did not reverse directions. or give me any new options to.
> Is there any testing that needs to be done? I've just received a new one of these and was hoping it would be easily integrated in to HA. The existing quick PR also has an unknown battery for me and did not reverse directions. or give me any new options to.

plus 1 - can help with testing, im using lumi.curtain.agl001, firmware 0x0000001b with sonoff Zigbee

Resolving patch:
diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py
--- a/zhaquirks/xiaomi/__init__.py
+++ b/zhaquirks/xiaomi/__init__.py
@@ -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 @@ def _update_battery_percentage(self, voltage_mv: int) -> None:
         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 @@ def _update_attribute(self, attrid, value):
         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
--- /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,
+                ],
+            }
+        },
+    }


Test patch:
diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py
--- a/tests/test_xiaomi.py
+++ b/tests/test_xiaomi.py
@@ -8,6 +8,7 @@
 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,
     DeviceTemperature,
@@ -51,6 +52,7 @@
     XiaomiQuickInitDevice,
     handle_quick_init,
 )
+import zhaquirks.xiaomi.aqara.driver_curtain_e1
 from zhaquirks.xiaomi.aqara.feeder_acn001 import (
     FEEDER_ATTR,
     ZCL_CHILD_LOCK,
@@ -1362,3 +1364,87 @@ async def test_xiaomi_t1_door_sensor(
     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].in_clusters[XiaomiAqaraE1Cluster.cluster_id]
+    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
+        )
+    )