Repository: zigpy/zha-device-handlers

Issue:
[Device Support Request] Aqara ceiling light L1-350 (ZNXDD01LM)
### Problem description

Device works fine in general. It connects and exposes light controls and a few configuration settings (see screenshot). However it doesn't allow to set power on behavior neither via dedicated config entity nor by setting `start_up_on_off` attribute of the `OnOff` cluster. 

I suspect this option is controlled by manufacturer specific cluster but have no idea how proceed. 

### Solution description

Either expose the power behavior configuration as an entity in homeassistant or at least make a available via setting attribute.

### Screenshots/Video

<details><summary>Screenshots/Video</summary>

![image](https://github.com/zigpy/zha-device-handlers/assets/5593615/9084c517-da1f-4efa-8085-d2cc2de6d579)

</details>


### Device signature

<details><summary>Device signature</summary>

```json
{
  "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",
      "input_clusters": [
        "0x0000",
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0006",
        "0x0008",
        "0x0300",
        "0xfcc0"
      ],
      "output_clusters": [
        "0x000a",
        "0x0019"
      ]
    }
  },
  "manufacturer": "Aqara",
  "model": "lumi.light.acn003",
  "class": "zigpy.device.Device"
}
```

</details>


### Diagnostic information

<details><summary>Diagnostic information</summary>

```json
[Paste the diagnostic information here]
```

</details>


### Logs

<details><summary>Logs</summary>

```python
[Paste the logs here]
```

</details>


### Custom quirk

<details><summary>Custom quirk</summary>

```python
from zigpy import types as t
from zigpy.profiles import zha
from zigpy.zcl.clusters.general import (
    Groups,
    Identify,
    OnOff,
    Ota,
    Scenes,
    Time, LevelControl,
)

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.xiaomi import (
    BasicCluster,
    XiaomiCustomDevice, XiaomiCluster,
)
from zigpy.zcl.clusters.lighting import Color
from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster


class OppleCluster(XiaomiCluster, ManufacturerSpecificCluster):
    """
    Add Opple cluster for power outage memory attribute.
    """
    cluster_id = 0xFCC0
    ep_attribute = "opple_cluster"
    attributes = {
        0x0201: ("power_outage_memory", t.Bool, True),
    }


class LumiLightAcn003(XiaomiCustomDevice):
    """
    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 OppleCluster.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
                    OppleCluster.cluster_id,  # 0xFCC0 - Manufacture Specific
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,  # 0x000A
                    Ota.cluster_id,  # 0x0019
                ]
            }
        }
    }
    replacement = {
        ENDPOINTS: {
            1: {
                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
                    OppleCluster,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,  # 0x000A
                    Ota.cluster_id,  # 0x0019
                ]
            }
        }
    }
```

</details>


### Additional information

_No response_


Discussion:
It seems like I found solution. This device supports `power_outage_memory` attribute from the Opple cluster. I updated original post with a quirk which works for me.

For those who want to make this light to preserve recent state after power loss:

1. Install custom quirks (put content of the quirk code from the post into python file under your custom quirks folder e.g. `custom_zha_quirks/acn003.py`
2. Restart home assistant
3. Check if your device uses quirk (under Zigbee Info you should see something like `Quirk: acn003.LumiLightAcn003
`). If you don't see this - something is wrong, there is no sense to proceed.
4. Open "Manage Zigbee Device" dialog, find cluster `OppleCluster`, select attribute `power_outage_memory`
5. Click "Read". If you see `Bool.false` change it to `Bool.true`.
For this device `power_outage_memory=true` means preserve state, `power_outage_memory=false` means start from ON on power restore.

Resolving patch:
diff --git a/zhaquirks/xiaomi/aqara/light_acn.py b/zhaquirks/xiaomi/aqara/light_acn.py
new file mode 100644
--- /dev/null
+++ b/zhaquirks/xiaomi/aqara/light_acn.py
@@ -0,0 +1,86 @@
+from zigpy import types as t
+from zigpy.profiles import zha
+from zigpy.zcl.clusters.general import (
+    Groups,
+    Identify,
+    LevelControl,
+    OnOff,
+    Ota,
+    Scenes,
+    Time,
+)
+from zigpy.zcl.clusters.lighting import Color
+
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+from zhaquirks.xiaomi import BasicCluster, XiaomiAqaraE1Cluster, XiaomiCustomDevice
+
+
+class OppleClusterLight(XiaomiAqaraE1Cluster):
+    """Add Opple cluster for power outage memory attribute."""
+
+    attributes = {
+        0x0201: ("power_outage_memory", t.Bool, True),
+    }
+
+
+class LumiLightAcn003(XiaomiCustomDevice):
+    """Quirk for Aqara ceiling light L1-350 also known as Xiaomi ZNXDD01LM.
+
+    Provides dimmable light control with color temperature setting.
+    This quirk adds support for power on behavior by adding the power_outage_memory attribute.
+    """
+
+    signature = {
+        MODELS_INFO: [
+            ("Aqara", "lumi.light.acn003"),
+        ],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
+                INPUT_CLUSTERS: [
+                    BasicCluster.cluster_id,  # 0x0000
+                    Identify.cluster_id,  # 0x0003
+                    Groups.cluster_id,  # 0x0004
+                    Scenes.cluster_id,  # 0x0005
+                    OnOff.cluster_id,  # 0x0006
+                    LevelControl.cluster_id,  # 0x0008
+                    Color.cluster_id,  # 0x0300
+                    OppleClusterLight.cluster_id,  # 0xFCC0 - manufacturer specific
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,  # 0x000A
+                    Ota.cluster_id,  # 0x0019
+                ],
+            }
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
+                INPUT_CLUSTERS: [
+                    BasicCluster,
+                    Identify.cluster_id,  # 0x0003
+                    Groups.cluster_id,  # 0x0004
+                    Scenes.cluster_id,  # 0x0005
+                    OnOff.cluster_id,  # 0x0006
+                    LevelControl.cluster_id,  # 0x0008
+                    Color.cluster_id,  # 0x0300
+                    OppleClusterLight,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,  # 0x000A
+                    Ota.cluster_id,  # 0x0019
+                ],
+            }
+        }
+    }


Test patch:
diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py
--- a/tests/test_xiaomi.py
+++ b/tests/test_xiaomi.py
@@ -1509,3 +1509,33 @@ async def test_xiaomi_e1_roller_commands_2(zigpy_device_from_quirk, command, val
     assert (
         analog_cluster._write_attributes.call_args[0][0][0].value.value == 100 - value
     )
+
+
+def test_aqara_acn003_signature_match(assert_signature_matches_quirk):
+    signature = {
+        "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered|RxOnWhenIdle|AllocateAddress: 142>, manufacturer_code=4447, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)",
+        "endpoints": {
+            "1": {
+                "profile_id": 0x0104,
+                "device_type": "0x0102",
+                "in_clusters": [
+                    "0x0000",
+                    "0x0003",
+                    "0x0004",
+                    "0x0005",
+                    "0x0006",
+                    "0x0008",
+                    "0x0300",
+                    "0xfcc0",
+                ],
+                "out_clusters": ["0x000a", "0x0019"],
+            }
+        },
+        "manufacturer": "Aqara",
+        "model": "lumi.light.acn003",
+        "class": "aqara_light.LumiLightAcn003",
+    }
+
+    assert_signature_matches_quirk(
+        zhaquirks.xiaomi.aqara.light_acn.LumiLightAcn003, signature
+    )