Repository: zigpy/zha-device-handlers (python)

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.


Pull request description:
Add Xiaomi E1 driver curtain support
## Proposed change
<!--
  Explain your proposed change below.
-->
~~https://github.com/zigpy/zha-device-handlers/pull/2620 should be merged first. This PR then needs to be rebased (for the test file).~~
This PR will be merged first, other than rebased. It'll use some cluster classes from this PR.

## Additional information
<!--
  Please include any additional information that is important to this PR.
  For example, if this PR is a potentially breaking change, mention that here.
  If this PR requires other PRs to be merged in HA Core or other projects, mention that.
  Lastly, if this PR fixes a specific issue, please include "Fixes #xxxx".
-->
Supersedes https://github.com/zigpy/zha-device-handlers/pull/1648
Fixes #2003

Changes are all untested. I'm also not sure if this is the right way to use the motor mode. It's in the original quirk and a HA PR for the inverted motor mode has already been merged (even though the quirk was never merged).

There are still some TODOs in the PR (which is also why `pre-commit` fails).

## 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
- [x] `pre-commit` checks pass / the code has been formatted using Black
- [x] Tests have been added to verify that the new code works


Patch:
diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py
index c945b36..e6a8a24 100644
--- 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 @@ 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,
+                ],
+            }
+        },
+    }


Test patch:
diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py
index 654ef9f..5a3b0ab 100644
--- a/tests/test_xiaomi.py
+++ b/tests/test_xiaomi.py
@@ -8,6 +8,7 @@ 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,
     DeviceTemperature,
@@ -51,6 +52,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,
@@ -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
+        )
+    )