Repository: zigpy/zha-device-handlers

Issue:
[Device Support Request] Decoupled mode for ctrl_neutral (QBKG04LM/QBKG03LM/QBKG21LM/QBKG22LM)
**Is your feature request related to a problem? Please describe.**
Similar to #365, but for `ctrl_neutral` implementation. I think that once implemented it can be easily adapted.

In my case I want to implement QBKG04LM decoupled mode.
It can be done easily, following @albalaing indications (https://github.com/zigpy/zha-device-handlers/issues/365#issuecomment-636510316).

The problem is that without the click button events it is not useful, so it is necessary to also implement the generation of ZHA events.

**Describe the solution you'd like**

Before implement anything you can see that device generate 4 messages:
```
2021-01-09 21:46:29 DEBUG (MainThread) [zigpy.device] Ignoring message (b'18500a0000100000001001') on cluster 6: unknown endpoint or cluster id: 4
2021-01-09 21:46:31 DEBUG (MainThread) [zigpy.device] Ignoring message (b'18510a00001000') on cluster 6: unknown endpoint or cluster id: 4
2021-01-09 21:46:32 DEBUG (MainThread) [zigpy.device] Ignoring message (b'18520a00001001') on cluster 6: unknown endpoint or cluster id: 4
2021-01-09 21:46:32 DEBUG (MainThread) [zigpy.device] Ignoring message (b'18530a00001002') on cluster 6: unknown endpoint or cluster id: 4
```

This messages are (in order):
* click
* hold
* release
* double_click

You can see that the click message corresponds in some way to the hold+release messages (00001000+00001001)


I think I'm halfway done (the decoupled part is working as expected), but I'm not satisfied with the event generation part. 


When a `replacement` for endpoint 4 (and 5) is implemented you can get the `on_off` attribute reported. But IMO this approach has the following problems:
* add a new entity in HA for click button events
* it is not possible to differentiate the `click` event from `hold` and `release`
* the `double_click` event cannot be handled (parse error)


Want I think that would be implemented is:
* no HA button entity
* full events control, not only hold and release


Any help in this regard is welcome.


**Device signature - this can be acquired by removing the device from ZHA and pairing it again from the add devices screen. Be sure to add the entire content of the log panel after pairing the device to a code block below this line.**
The device signature is known and correct.

**Additional context**
Although I am not sure if it can be done this way, I have tried without success to implement a replacement that transforms events from the `OnOff` cluster to the `MultistateInput` cluster.

My current implementation is as follows (only the relevant part):
```
    class BasicClusterDecoupled(BasicCluster):
        """Adds attributes for decoupled mode"""
        def __init__(self, *args, **kwargs):
            """Init."""
            self.attributes = BasicCluster.attributes.copy()
            self.attributes.update({ 0xFF22: ("left_decoupled_mode", t.uint8_t)})
            self.attributes.update({ 0xFF23: ("right_decoupled_mode", t.uint8_t)})
            super().__init__(*args, **kwargs)


    class CustomOnOffCluster(OnOffCluster):
        """Fire ZHA events for on off cluster."""

        cluster_id = OnOff.cluster_id

        def __init__(self, *args, **kwargs):
            """Init."""
            self._current_state = {}
            super().__init__(*args, **kwargs)

        def _update_attribute(self, attrid, value):
            _LOGGER.info("%s: %s", attrid, value)
            if attrid == 0:
                self._current_state = PRESS_TYPES.get(value)
                event_args = {
                    PRESS_TYPE: self._current_state,
                    VALUE: value,
                }
                self.listener_event(ZHA_SEND_EVENT, COMMAND_CLICK, event_args)
            super()._update_attribute(attrid, self._current_state)


    replacement = {
        SKIP_CONFIGURATION: True,
        ENDPOINTS: {
            .../...
            4: {                                                                   
                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,                         
                INPUT_CLUSTERS: [                                                  
                    MultistateInput.cluster_id,                                        
                    CustomOnOffCluster,                                        
                ],                                                                 
                OUTPUT_CLUSTERS: [],                                               
            },  
            5: {                                                                   
                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,                         
                INPUT_CLUSTERS: [                                                  
                    MultistateInput.cluster_id,                                        
                    CustomOnOffCluster,                                        
                ],                                                                 
                OUTPUT_CLUSTERS: [],                                               
            },  
        },
    }
```




Discussion:

Resolving patch:
diff --git a/zhaquirks/xiaomi/aqara/ctrl_neutral.py b/zhaquirks/xiaomi/aqara/ctrl_neutral.py
--- a/zhaquirks/xiaomi/aqara/ctrl_neutral.py
+++ b/zhaquirks/xiaomi/aqara/ctrl_neutral.py
@@ -1,6 +1,7 @@
 """Xiaomi aqara single key wall switch devices."""
 import logging
 
+from zigpy import types as t
 from zigpy.profiles import zha
 from zigpy.zcl.clusters.general import (
     AnalogInput,
@@ -23,16 +24,30 @@
     XiaomiCustomDevice,
     XiaomiPowerConfiguration,
 )
+from ... import EventableCluster
 from ...const import (
+    ARGS,
+    ATTRIBUTE_ID,
+    ATTRIBUTE_NAME,
+    BUTTON,
+    CLUSTER_ID,
+    COMMAND,
+    COMMAND_ATTRIBUTE_UPDATED,
+    COMMAND_DOUBLE,
+    COMMAND_HOLD,
+    COMMAND_RELEASE,
     DEVICE_TYPE,
+    ENDPOINT_ID,
     ENDPOINTS,
     INPUT_CLUSTERS,
     MODELS_INFO,
     OUTPUT_CLUSTERS,
     PROFILE_ID,
     SKIP_CONFIGURATION,
+    VALUE,
 )
 
+ATTRIBUTE_ON_OFF = "on_off"
 DOUBLE = "double"
 HOLD = "long press"
 PRESS_TYPES = {0: "long press", 1: "single", 2: "double"}
@@ -53,6 +68,20 @@
 class CtrlNeutral(XiaomiCustomDevice):
     """Aqara single and double key switch device."""
 
+    class BasicClusterDecoupled(BasicCluster):
+        """Adds attributes for decoupled mode."""
+
+        # Known Options for 'decoupled_mode_<button>':
+        # * 254 (decoupled)
+        # * 18 (relay controlled)
+        manufacturer_attributes = {
+            0xFF22: ("decoupled_mode_left", t.uint8_t),
+            0xFF23: ("decoupled_mode_right", t.uint8_t),
+        }
+
+    class WallSwitchOnOffCluster(EventableCluster, OnOff):
+        """WallSwitchOnOffCluster: fire events corresponding to press type."""
+
     signature = {
         MODELS_INFO: [
             (LUMI, "lumi.ctrl_neutral1"),
@@ -157,7 +186,7 @@ class CtrlNeutral(XiaomiCustomDevice):
             1: {
                 DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
                 INPUT_CLUSTERS: [
-                    BasicCluster,
+                    BasicClusterDecoupled,
                     Identify.cluster_id,
                     XiaomiPowerConfiguration.cluster_id,
                     DeviceTemperature.cluster_id,
@@ -186,5 +215,34 @@ class CtrlNeutral(XiaomiCustomDevice):
                 ],
                 OUTPUT_CLUSTERS: [],
             },
+            4: {
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    MultistateInput.cluster_id,
+                    WallSwitchOnOffCluster,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+        },
+    }
+
+    device_automation_triggers = {
+        (COMMAND_HOLD, BUTTON): {
+            ENDPOINT_ID: 4,
+            CLUSTER_ID: 6,
+            COMMAND: COMMAND_ATTRIBUTE_UPDATED,
+            ARGS: {ATTRIBUTE_ID: 0, ATTRIBUTE_NAME: ATTRIBUTE_ON_OFF, VALUE: 0},
+        },
+        (COMMAND_RELEASE, BUTTON): {
+            ENDPOINT_ID: 4,
+            CLUSTER_ID: 6,
+            COMMAND: COMMAND_ATTRIBUTE_UPDATED,
+            ARGS: {ATTRIBUTE_ID: 0, ATTRIBUTE_NAME: ATTRIBUTE_ON_OFF, VALUE: 1},
+        },
+        (COMMAND_DOUBLE, BUTTON): {
+            ENDPOINT_ID: 4,
+            CLUSTER_ID: 6,
+            COMMAND: COMMAND_ATTRIBUTE_UPDATED,
+            ARGS: {ATTRIBUTE_ID: 0, ATTRIBUTE_NAME: ATTRIBUTE_ON_OFF, VALUE: 2},
         },
     }


Test patch: