Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Aqara 2 Gang switch without neutral (lumi.switch.l2aeu1) #2270

Closed
sophipl opened this issue Mar 12, 2023 · 13 comments
Closed

Support for Aqara 2 Gang switch without neutral (lumi.switch.l2aeu1) #2270

sophipl opened this issue Mar 12, 2023 · 13 comments
Labels
stale Issue is inactivate and might get closed soon Xiaomi Request/PR regarding a Xiaomi device

Comments

@sophipl
Copy link

sophipl commented Mar 12, 2023

I would like to have the ability to trigger on double button press, which I read that this device have in Zigbee2MQTT mode.
The device has automation abilities in HA on state changes (light on, light off), but not of button presses, apparently you can have the ability to disconnect button from it's internal relay and automate it differently as well.

I didn't see any pre-existing quirks.
Sadly I understand too zigbee protocol too little to write this quirk myself.

Device 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: 128>, 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=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": 260,
      "device_type": "0x0100",
      "in_clusters": [
        "0x0000",
        "0x0002",
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0006",
        "0x0009"
      ],
      "out_clusters": [
        "0x000a",
        "0x0019"
      ]
    },
    "2": {
      "profile_id": 260,
      "device_type": "0x0100",
      "in_clusters": [
        "0x0000",
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0006",
        "0x0012",
        "0xfcc0"
      ],
      "out_clusters": []
    },
    "242": {
      "profile_id": 41440,
      "device_type": "0x0061",
      "in_clusters": [],
      "out_clusters": [
        "0x0021"
      ]
    }
  },
  "manufacturer": "LUMI",
  "model": "lumi.switch.l2aeu1",
  "class": "zigpy.device.Device"
}
Diagnostic information
{
  "home_assistant": {
    "installation_type": "Home Assistant Container",
    "version": "2023.1.6",
    "dev": false,
    "hassio": false,
    "virtualenv": false,
    "python_version": "3.10.7",
    "docker": true,
    "arch": "aarch64",
    "timezone": "Europe/Warsaw",
    "os_name": "Linux",
    "os_version": "5.15.84-v8+",
    "run_as_root": true
  },
  "custom_components": {},
  "integration_manifest": {
    "domain": "zha",
    "name": "Zigbee Home Automation",
    "config_flow": true,
    "documentation": "https://www.home-assistant.io/integrations/zha",
    "requirements": [
      "bellows==0.34.6",
      "pyserial==3.5",
      "pyserial-asyncio==0.6",
      "zha-quirks==0.0.90",
      "zigpy-deconz==0.19.2",
      "zigpy==0.53.0",
      "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*"
      },
      {
        "type": "_slzb-06._tcp.local.",
        "name": "slzb-06*"
      }
    ],
    "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": 41887,
    "manufacturer": "LUMI",
    "model": "lumi.switch.l2aeu1",
    "name": "LUMI lumi.switch.l2aeu1",
    "quirk_applied": false,
    "quirk_class": "zigpy.device.Device",
    "manufacturer_code": 4447,
    "power_source": "Battery or Unknown",
    "lqi": 156,
    "rssi": -61,
    "last_seen": "2023-03-12T17:09:49",
    "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: 128>, 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=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
      "endpoints": {
        "1": {
          "profile_id": 260,
          "device_type": "0x0100",
          "in_clusters": [
            "0x0000",
            "0x0002",
            "0x0003",
            "0x0004",
            "0x0005",
            "0x0006",
            "0x0009"
          ],
          "out_clusters": [
            "0x000a",
            "0x0019"
          ]
        },
        "2": {
          "profile_id": 260,
          "device_type": "0x0100",
          "in_clusters": [
            "0x0000",
            "0x0003",
            "0x0004",
            "0x0005",
            "0x0006",
            "0x0012",
            "0xfcc0"
          ],
          "out_clusters": []
        },
        "242": {
          "profile_id": 41440,
          "device_type": "0x0061",
          "in_clusters": [],
          "out_clusters": [
            "0x0021"
          ]
        }
      }
    },
    "active_coordinator": false,
    "entities": [
      {
        "entity_id": "button.lumi_lumi_switch_l2aeu1_identify",
        "name": "LUMI lumi.switch.l2aeu1"
      },
      {
        "entity_id": "light.lumi_lumi_switch_l2aeu1_light",
        "name": "LUMI lumi.switch.l2aeu1"
      },
      {
        "entity_id": "light.lumi_lumi_switch_l2aeu1_light_2",
        "name": "LUMI lumi.switch.l2aeu1"
      },
      {
        "entity_id": "sensor.lumi_lumi_switch_l2aeu1_device_temperature",
        "name": "LUMI lumi.switch.l2aeu1"
      },
      {
        "entity_id": "sensor.lumi_lumi_switch_l2aeu1_rssi",
        "name": "LUMI lumi.switch.l2aeu1"
      }
    ],
    "neighbors": [],
    "routes": [],
    "endpoint_names": [
      {
        "name": "ON_OFF_LIGHT"
      },
      {
        "name": "ON_OFF_LIGHT"
      },
      {
        "name": "unknown 97 device_type of 0xa1e0 profile id"
      }
    ],
    "user_given_name": "\u015awiat\u0142o \u0142azienka",
    "device_reg_id": "9e67232a205c08488d57d75848d40f27",
    "area_id": "lazienka",
    "cluster_details": {
      "1": {
        "device_type": {
          "name": "ON_OFF_LIGHT",
          "id": 256
        },
        "profile_id": 260,
        "in_clusters": {
          "0x0000": {
            "endpoint_attribute": "basic",
            "attributes": {
              "0x0004": {
                "attribute_name": "manufacturer",
                "value": "LUMI"
              },
              "0x0005": {
                "attribute_name": "model",
                "value": "lumi.switch.l2aeu1"
              },
              "0x0006": {
                "attribute_name": "date_code",
                "value": "01-12-2021"
              }
            },
            "unsupported_attributes": {}
          },
          "0x0002": {
            "endpoint_attribute": "device_temperature",
            "attributes": {
              "0x0000": {
                "attribute_name": "current_temperature",
                "value": 30
              }
            },
            "unsupported_attributes": {}
          },
          "0x0003": {
            "endpoint_attribute": "identify",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0004": {
            "endpoint_attribute": "groups",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0005": {
            "endpoint_attribute": "scenes",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0006": {
            "endpoint_attribute": "on_off",
            "attributes": {
              "0x0000": {
                "attribute_name": "on_off",
                "value": 0
              }
            },
            "unsupported_attributes": {
              "0x4003": {
                "attribute_name": "start_up_on_off"
              }
            }
          },
          "0x0009": {
            "endpoint_attribute": "alarms",
            "attributes": {},
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {
          "0x000a": {
            "endpoint_attribute": "time",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0019": {
            "endpoint_attribute": "ota",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      },
      "2": {
        "device_type": {
          "name": "ON_OFF_LIGHT",
          "id": 256
        },
        "profile_id": 260,
        "in_clusters": {
          "0x0000": {
            "endpoint_attribute": "basic",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0003": {
            "endpoint_attribute": "identify",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0004": {
            "endpoint_attribute": "groups",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0005": {
            "endpoint_attribute": "scenes",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0006": {
            "endpoint_attribute": "on_off",
            "attributes": {
              "0x0000": {
                "attribute_name": "on_off",
                "value": 0
              }
            },
            "unsupported_attributes": {
              "0x4003": {
                "attribute_name": "start_up_on_off"
              }
            }
          },
          "0x0012": {
            "endpoint_attribute": "multistate_input",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0xfcc0": {
            "endpoint_attribute": "manufacturer_specific",
            "attributes": {},
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {}
      },
      "242": {
        "device_type": {
          "name": "unknown",
          "id": 97
        },
        "profile_id": 41440,
        "in_clusters": {},
        "out_clusters": {
          "0x0021": {
            "endpoint_attribute": "green_power",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      }
    }
  }
}
@sophipl
Copy link
Author

sophipl commented Mar 24, 2023

Well, I think I got as far as I could
The cluster commands works fine, I can detach relay, invert LEDs, disable anti-flicker
But buttons doesn't work
I sit with wireshark and HA logs, didn't manage to make it work.
And it's maddening, everything is a string in quirk device_automation_triggers, how can you know how to match things and from where they came from? There is something to be said about strongly typed languages.

Quirk so far
import copy
from enum import Enum

from zigpy import types as t
from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
from zhaquirks import CustomCluster
from zigpy.zcl.clusters.general import (
    Basic,
    DeviceTemperature,
    Groups,
    Identify,
    OnOff,
    Ota,
    Scenes,
    Time,
    MultistateInput,
    GreenPowerProxy,
    Alarms,
)

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
    SHORT_PRESS,
    TRIPLE_PRESS,
    LONG_PRESS,
    ALT_DOUBLE_PRESS,
    DOUBLE_PRESS,
    ALT_SHORT_PRESS,
    LEFT,
    RIGHT,
    COMMAND_TOGGLE,
    COMMAND,
    BUTTON,
    ENDPOINT_ID,
    ARGS,
    CLUSTER_ID,
    ATTR_ID,
    PRESS_TYPE,
    VALUE,
)
from zhaquirks.xiaomi import (
    LUMI
)

COMMAND_SINGLE = "single"
COMMAND_DOUBLE = "double"
COMMAND_BUTTON_HOLD = "button_hold"

from zhaquirks.xiaomi.aqara.opple_switch import (OppleSwitchCluster, BOTH_BUTTONS)
from zhaquirks.xiaomi.aqara.opple_remote import (
    MultistateInputCluster,
    OppleCluster,
    OPPLE_CLUSTER_ID
)


class XiaomiAqaraOpMode(t.uint8_t, Enum):
    Decoupled = 0x00
    Coupled = 0x01

class XiaomiAqaraFlickerMode(t.uint16_t, Enum):
    Anti_flicker = 0x0004
    Quick = 0x0001

class XiaomiAqaraIndicatorLight(t.uint8_t, Enum):
    Normal = 0x00
    Reverse = 0x01

class XiaomiAqaraOperationMode(t.uint8_t, Enum):
    """Opple operation_mode enum."""

    Decoupled = 0x00
    Coupled = 0x01


class XiaomiAqaraMasterCluster(OppleCluster):

    attributes = copy.deepcopy(OppleCluster.attributes)

    attributes.update(
        {
            0x0004: ("anti_flicker", XiaomiAqaraFlickerMode, True),
            0x0002: ("power_outage_count", t.uint8_t, True),
            0x0201: ("power_outage_memory", t.Bool, True),
            0x0203: ("led_disabled_night", t.Bool, True),
            0x00F0: ("indicator_light", XiaomiAqaraIndicatorLight, True),
            0x0200: ("operation_mode", XiaomiAqaraOperationMode, True),
        }
    )


class XiaomiAqaraSecondaryCluster(CustomCluster):
    ep_attribute = "opple_cluster"
    cluster_id = OPPLE_CLUSTER_ID

    attributes = (
        {
            0x0200: ("operation_mode", XiaomiAqaraOperationMode, True), 
        }
    )


class XiaomiAqaraWSEUK02(CustomDevice):
    """WS-EUK02 Aqara smart wall switch H1 EU (no neutral, double rocker)"""

    signature = {
        MODELS_INFO: [(LUMI, "lumi.switch.l2aeu1")],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    DeviceTemperature.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    OnOff.cluster_id,
                    Alarms.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
            2: {
                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,
                    MultistateInput.cluster_id,
                    OppleSwitchCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                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,
                    DeviceTemperature.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    OnOff.cluster_id,
                    Alarms.cluster_id,
                    XiaomiAqaraMasterCluster,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
            2: {
                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,
                    MultistateInputCluster,
                    XiaomiAqaraSecondaryCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

    device_automation_triggers = {
        (SHORT_PRESS, LEFT): {ENDPOINT_ID:41,  CLUSTER_ID : MultistateInputCluster.cluster_id, ARGS: {ATTR_ID: 0x0055, PRESS_TYPE: COMMAND_SINGLE, VALUE: 1}},
        (DOUBLE_PRESS, LEFT): {ENDPOINT_ID:41, CLUSTER_ID : MultistateInputCluster.cluster_id, ARGS: {ATTR_ID: 0x0055, PRESS_TYPE: COMMAND_DOUBLE, VALUE: 2}},
        (SHORT_PRESS, RIGHT): {ENDPOINT_ID:42, CLUSTER_ID : MultistateInputCluster.cluster_id, ARGS: {ATTR_ID: 0x0055, PRESS_TYPE: COMMAND_SINGLE, VALUE: 1}},
        (DOUBLE_PRESS, RIGHT): {ENDPOINT_ID:42, CLUSTER_ID : MultistateInputCluster.cluster_id, ARGS: {ATTR_ID: 0x0055, PRESS_TYPE: COMMAND_DOUBLE, VALUE: 2}},
        (SHORT_PRESS, BOTH_BUTTONS): {ENDPOINT_ID:42, CLUSTER_ID : MultistateInputCluster.cluster_id, ARGS: {ATTR_ID: 0x0055, PRESS_TYPE: COMMAND_SINGLE, VALUE: 1}},
        (DOUBLE_PRESS, BOTH_BUTTONS): {ENDPOINT_ID:42, CLUSTER_ID : MultistateInputCluster.cluster_id, ARGS: {ATTR_ID: 0x0055, PRESS_TYPE: COMMAND_DOUBLE, VALUE: 2}},
        (COMMAND_BUTTON_HOLD, BOTH_BUTTONS): {ENDPOINT_ID: 1, CLUSTER_ID: XiaomiAqaraMasterCluster.cluster_id, ARGS: {ATTR_ID: 0x00FC, VALUE: 0, PRESS_TYPE: COMMAND_BUTTON_HOLD}},
    }

The buttons work like this, left button is source_endpoint 41, right button is source_endpoint 42, both buttons is 51
It is sending value of uint16 of 1 or 2, for one press or 2 presses, attribute id 0x0055 (Preset value)
When you press 3 times, it does something funny, it introduces itself, sends string value with model information.
Holding a button sends data for cluster 0xfcc0 with value false of boolean, attribute id 0x00fc

It seems that ZHA cannot decode the values as event, I don't know, just a packet


2023-03-24 22:06:34.973 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xD3A6), src_ep=41, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000),dst_ep=1, source_route=None, extended_timeout=False, tsn=116, profile_id=260, cluster_id=18, data=Serialized[b'\x18>\nU\x00!\x02\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=255, rssi=-34)
2023-03-24 22:05:06.802 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xD3A6), src_ep=41, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000),dst_ep=1, source_route=None, extended_timeout=False, tsn=109, profile_id=260, cluster_id=18, data=Serialized[b'\x188\nU\x00!\x01\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=255, rssi=-33)
2023-03-24 22:05:08.801 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xD3A6), src_ep=41, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000),dst_ep=1, source_route=None, extended_timeout=False, tsn=110, profile_id=260, cluster_id=18, data=Serialized[b'\x189\nU\x00!\x01\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=255, rssi=-33)
2023-03-24 22:39:43.094 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xD3A6), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=161, profile_id=260, cluster_id=64704, data=Serialized[b'\x1c_\x11L\n\xfc\x00\x10\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=255, rssi=-34)
2023-03-24 22:05:09.654 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xD3A6), src_ep=42, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000),dst_ep=1, source_route=None, extended_timeout=False, tsn=112, profile_id=260, cluster_id=18, data=Serialized[b'\x18;\nU\x00!\x01\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=255, rssi=-33)
2023-03-24 22:25:20.180 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0xD3A6), src_ep=51, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=141, profile_id=260, cluster_id=18, data=Serialized[b'\x18E\nU\x00!\x01\x00'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=255, rssi=-34)

Maybe you will be able to fix the problem, my head explodes now :)

@sophipl
Copy link
Author

sophipl commented Mar 25, 2023

I managed to do it, it took some effort
I don't know why device_automation_triggers didn't wanted to match at some point of my development, so I limited it to COMMAND

If someone want's to use it, please go ahead

aqara_i2aeu1.py
import copy
from enum import Enum

from zigpy import types as t
from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
from zhaquirks import CustomCluster
from zigpy.zcl.clusters.general import (
    Basic,
    DeviceTemperature,
    Groups,
    Identify,
    OnOff,
    Ota,
    Scenes,
    Time,
    MultistateInput,
    GreenPowerProxy,
    Alarms,
)

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
    SHORT_PRESS,
    TRIPLE_PRESS,
    LONG_PRESS,
    ALT_DOUBLE_PRESS,
    DOUBLE_PRESS,
    ALT_SHORT_PRESS,
    LEFT,
    RIGHT,
    COMMAND_TOGGLE,
    COMMAND,
    BUTTON,
    ENDPOINT_ID,
    ARGS,
    CLUSTER_ID,
    ATTR_ID,
    PRESS_TYPE,
    VALUE,
    ZHA_SEND_EVENT,
    COMMAND_HOLD,
    COMMAND_TRIPLE
)
from zhaquirks.xiaomi import (
    LUMI
)

COMMAND_SINGLE = "single"
COMMAND_DOUBLE = "double"
BUTTON_ANY = "any"
LEFT_BUTTON_ENDPOINT=41
RIGHT_BUTTON_ENDPOINT=42
BOTH_BUTTONS_ENDPOINT=51

from zhaquirks.xiaomi.aqara.opple_switch import (OppleSwitchCluster, BOTH_BUTTONS)
from zhaquirks.xiaomi.aqara.opple_remote import (
    OppleCluster,
    OPPLE_CLUSTER_ID,
    STATUS_TYPE_ATTR,
    PRESS_TYPES
)


class XiaomiAqaraOpMode(t.uint8_t, Enum):
    Decoupled = 0x00
    Coupled = 0x01

class XiaomiAqaraFlickerMode(t.uint16_t, Enum):
    Anti_flicker = 0x0004
    Quick = 0x0001

class XiaomiAqaraIndicatorLight(t.uint8_t, Enum):
    Normal = 0x00
    Reverse = 0x01

class XiaomiAqaraOperationMode(t.uint8_t, Enum):
    Decoupled = 0x00
    Coupled = 0x01


class XiaomiAqaraMasterCluster(OppleCluster):

    attributes = copy.deepcopy(OppleCluster.attributes)

    attributes.update(
        {
            0x0004: ("anti_flicker", XiaomiAqaraFlickerMode, True),
            0x0002: ("power_outage_count", t.uint8_t, True),
            0x0201: ("power_outage_memory", t.Bool, True),
            0x0203: ("led_disabled_night", t.Bool, True),
            0x00F0: ("indicator_light", XiaomiAqaraIndicatorLight, True),
            0x0200: ("operation_mode", XiaomiAqaraOperationMode, True),
        }
    )

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == 0x00FC and value == False:
            event_args = {
                BUTTON: BUTTON_ANY,
                PRESS_TYPE: COMMAND_HOLD,
                ATTR_ID: attrid,
                VALUE: value,
            }
            action = "{}_{}".format(BUTTON_ANY, COMMAND_HOLD)
            self.listener_event(ZHA_SEND_EVENT, action, event_args)
            super()._update_attribute(0, action)
#        elif attrid == 0x00f7:
#            event_args = {
#                BUTTON: BUTTON_ANY,
#                PRESS_TYPE: COMMAND_TRIPLE,
#                ATTR_ID: attrid
#            }
#            action = "{}_{}".format(BUTTON_ANY, COMMAND_TRIPLE)
#            self.listener_event(ZHA_SEND_EVENT, action, event_args)
#            super()._update_attribute(0, action)


class XiaomiAqaraSecondaryCluster(CustomCluster):
    ep_attribute = "opple_cluster"
    cluster_id = OPPLE_CLUSTER_ID

    attributes = (
        {
            0x0200: ("operation_mode", XiaomiAqaraOperationMode, True), 
        }
    )

class MultistateInputCluster(CustomCluster, MultistateInput):
    cluster_id = MultistateInput.cluster_id

    def __init__(self, *args, **kwargs):
        self._current_state = None
        super().__init__(*args, **kwargs)

    async def configure_reporting(
        self,
        attribute,
        min_interval,
        max_interval,
        reportable_change,
        manufacturer=None,
    ): """Configure reporting."""

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == STATUS_TYPE_ATTR:
            self._current_state = PRESS_TYPES.get(value)
            event_args = {
                BUTTON: self.endpoint.endpoint_id,
                PRESS_TYPE: self._current_state,
                ATTR_ID: attrid,
                VALUE: value,
            }
            button = None

            if self.endpoint.endpoint_id == LEFT_BUTTON_ENDPOINT:
                button=LEFT
            elif self.endpoint.endpoint_id == RIGHT_BUTTON_ENDPOINT:
                button=RIGHT
            elif self.endpoint.endpoint_id == BOTH_BUTTONS_ENDPOINT:
                button=BOTH_BUTTONS
            

            action=action = "{}_{}".format(self.endpoint.endpoint_id, self._current_state)
            if button != None:
                action = "{}_{}".format(button, self._current_state)

            self.listener_event(ZHA_SEND_EVENT, action, event_args)
            super()._update_attribute(0, action)


class XiaomiAqaraWSEUK02(CustomDevice):
    """WS-EUK02 Aqara smart wall switch H1 EU (no neutral, double rocker)"""

    signature = {
        MODELS_INFO: [(LUMI, "lumi.switch.l2aeu1")],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    DeviceTemperature.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    OnOff.cluster_id,
                    Alarms.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
            2: {
                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,
                    MultistateInput.cluster_id,
                    OppleSwitchCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                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,
                    DeviceTemperature.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    OnOff.cluster_id,
                    Alarms.cluster_id,
                    MultistateInputCluster,
                    XiaomiAqaraMasterCluster,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
            2: {
                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,
                    MultistateInputCluster,
                    XiaomiAqaraSecondaryCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            LEFT_BUTTON_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInputCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            RIGHT_BUTTON_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInputCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            BOTH_BUTTONS_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInputCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

    device_automation_triggers = {
        (SHORT_PRESS, LEFT): { COMMAND: "{}_{}".format(LEFT, COMMAND_SINGLE)},
        (DOUBLE_PRESS, LEFT): { COMMAND: "{}_{}".format(LEFT, COMMAND_DOUBLE)},
        (SHORT_PRESS, RIGHT): { COMMAND: "{}_{}".format(RIGHT, COMMAND_SINGLE)},
        (DOUBLE_PRESS, RIGHT): { COMMAND: "{}_{}".format(RIGHT, COMMAND_DOUBLE)},
        (SHORT_PRESS, BOTH_BUTTONS): { COMMAND: "{}_{}".format(BOTH_BUTTONS, COMMAND_SINGLE)},
        (DOUBLE_PRESS, BOTH_BUTTONS): { COMMAND: "{}_{}".format(BOTH_BUTTONS, COMMAND_DOUBLE)},
        (COMMAND_HOLD, BUTTON): { COMMAND: "{}_{}".format(BUTTON_ANY, COMMAND_HOLD)},
    }

@sophipl sophipl closed this as completed Mar 25, 2023
@sophipl
Copy link
Author

sophipl commented Mar 25, 2023

FYI @TheJulianJES
If someone thinks it's worth integrating

@TheJulianJES TheJulianJES reopened this Mar 25, 2023
@TheJulianJES
Copy link
Collaborator

We can leave this open for now (until zha-quirks maybe fixes this).

@grzegor
Copy link

grzegor commented Apr 17, 2023

@sophipl thank you! Decoupling worked and both the single and double press automations work flawlessly!

@sophipl
Copy link
Author

sophipl commented Apr 17, 2023

@grzegor Thanks! :)

@cpressland
Copy link

cpressland commented Apr 30, 2023

@sophipl hello! I just received my lumi.switch.l2aeu1 today. I've loaded your quirk into Home Assistant and can confirm it's being used by ZHA. The only thing I'm not sure about is how to put the device in decoupled mode, any advice you can give me?

EDIT: I can see two clusters: XiaomiAqaraMasterCluster & XiaomiAqaraSecondaryCluster, and within each there is an operation_mode (id: 0x0200), Reading the value of this on either returns XiaomiAqaraOperationMode.Coupled. So I'm absolutely in the right place, I just can't figure out what to write back to move it to Decoupled.

EDIT2: Send a zero. I tried everything except writing a zero. Sorry! Thanks for the Quirk. You're awesome.

@sophipl
Copy link
Author

sophipl commented May 1, 2023

@cpressland Great that you figured out!
I don't think there is a way to export the value into selection box in HA, so we have to stumble in the menus. :)

@Leworoe
Copy link

Leworoe commented May 25, 2023

I needed to edit this quirk. I don't know why my switch has more ENDPOINTS in the signature. The INPUT_CLUSTERS in the first endpoint differ.

Device 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: 128>, 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=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": "0x0104",
      "device_type": "0x0100",
      "input_clusters": [
        "0x0000",
        "0x0002",
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0006",
        "0x0012",
        "0xfcc0"
      ],
      "output_clusters": [
        "0x000a",
        "0x0019"
      ]
    },
    "2": {
      "profile_id": "0x0104",
      "device_type": "0x0100",
      "input_clusters": [
        "0x0000",
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0006",
        "0x0012",
        "0xfcc0"
      ],
      "output_clusters": []
    },
    "41": {
      "profile_id": "0x0104",
      "device_type": "0x0100",
      "input_clusters": [
        "0x0012"
      ],
      "output_clusters": []
    },
    "42": {
      "profile_id": "0x0104",
      "device_type": "0x0100",
      "input_clusters": [
        "0x0012"
      ],
      "output_clusters": []
    },
    "51": {
      "profile_id": "0x0104",
      "device_type": "0x0100",
      "input_clusters": [
        "0x0012"
      ],
      "output_clusters": []
    },
    "242": {
      "profile_id": "0xa1e0",
      "device_type": "0x0061",
      "input_clusters": [],
      "output_clusters": [
        "0x0021"
      ]
    }
  },
  "manufacturer": "LUMI",
  "model": "lumi.switch.l2aeu1"
}
The edited quirk
import copy
from enum import Enum

from zigpy import types as t
from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
from zhaquirks import CustomCluster
from zigpy.zcl.clusters.general import (
    Basic,
    DeviceTemperature,
    Groups,
    Identify,
    OnOff,
    Ota,
    Scenes,
    Time,
    MultistateInput,
    GreenPowerProxy,
    Alarms,
)

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
    SHORT_PRESS,
    TRIPLE_PRESS,
    LONG_PRESS,
    ALT_DOUBLE_PRESS,
    DOUBLE_PRESS,
    ALT_SHORT_PRESS,
    LEFT,
    RIGHT,
    COMMAND_TOGGLE,
    COMMAND,
    BUTTON,
    ENDPOINT_ID,
    ARGS,
    CLUSTER_ID,
    ATTR_ID,
    PRESS_TYPE,
    VALUE,
    ZHA_SEND_EVENT,
    COMMAND_HOLD,
    COMMAND_TRIPLE
)
from zhaquirks.xiaomi import (
    LUMI
)

COMMAND_SINGLE = "single"
COMMAND_DOUBLE = "double"
BUTTON_ANY = "any"
LEFT_BUTTON_ENDPOINT=41
RIGHT_BUTTON_ENDPOINT=42
BOTH_BUTTONS_ENDPOINT=51

from zhaquirks.xiaomi.aqara.opple_switch import (OppleSwitchCluster, BOTH_BUTTONS)
from zhaquirks.xiaomi.aqara.opple_remote import (
    OppleCluster,
    OPPLE_CLUSTER_ID,
    STATUS_TYPE_ATTR,
    PRESS_TYPES
)


class XiaomiAqaraOpMode(t.uint8_t, Enum):
    Decoupled = 0x00
    Coupled = 0x01

class XiaomiAqaraFlickerMode(t.uint16_t, Enum):
    Anti_flicker = 0x0004
    Quick = 0x0001

class XiaomiAqaraIndicatorLight(t.uint8_t, Enum):
    Normal = 0x00
    Reverse = 0x01

class XiaomiAqaraOperationMode(t.uint8_t, Enum):
    Decoupled = 0x00
    Coupled = 0x01


class XiaomiAqaraMasterCluster(OppleCluster):

    attributes = copy.deepcopy(OppleCluster.attributes)

    attributes.update(
        {
            0x0004: ("anti_flicker", XiaomiAqaraFlickerMode, True),
            0x0002: ("power_outage_count", t.uint8_t, True),
            0x0201: ("power_outage_memory", t.Bool, True),
            0x0203: ("led_disabled_night", t.Bool, True),
            0x00F0: ("indicator_light", XiaomiAqaraIndicatorLight, True),
            0x0200: ("operation_mode", XiaomiAqaraOperationMode, True),
        }
    )

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == 0x00FC and value == False:
            event_args = {
                BUTTON: BUTTON_ANY,
                PRESS_TYPE: COMMAND_HOLD,
                ATTR_ID: attrid,
                VALUE: value,
            }
            action = "{}_{}".format(BUTTON_ANY, COMMAND_HOLD)
            self.listener_event(ZHA_SEND_EVENT, action, event_args)
            super()._update_attribute(0, action)
#        elif attrid == 0x00f7:
#            event_args = {
#                BUTTON: BUTTON_ANY,
#                PRESS_TYPE: COMMAND_TRIPLE,
#                ATTR_ID: attrid
#            }
#            action = "{}_{}".format(BUTTON_ANY, COMMAND_TRIPLE)
#            self.listener_event(ZHA_SEND_EVENT, action, event_args)
#            super()._update_attribute(0, action)


class XiaomiAqaraSecondaryCluster(CustomCluster):
    ep_attribute = "opple_cluster"
    cluster_id = OPPLE_CLUSTER_ID

    attributes = (
        {
            0x0200: ("operation_mode", XiaomiAqaraOperationMode, True), 
        }
    )

class MultistateInputCluster(CustomCluster, MultistateInput):
    cluster_id = MultistateInput.cluster_id

    def __init__(self, *args, **kwargs):
        self._current_state = None
        super().__init__(*args, **kwargs)

    async def configure_reporting(
        self,
        attribute,
        min_interval,
        max_interval,
        reportable_change,
        manufacturer=None,
    ): """Configure reporting."""

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == STATUS_TYPE_ATTR:
            self._current_state = PRESS_TYPES.get(value)
            event_args = {
                BUTTON: self.endpoint.endpoint_id,
                PRESS_TYPE: self._current_state,
                ATTR_ID: attrid,
                VALUE: value,
            }
            button = None

            if self.endpoint.endpoint_id == LEFT_BUTTON_ENDPOINT:
                button=LEFT
            elif self.endpoint.endpoint_id == RIGHT_BUTTON_ENDPOINT:
                button=RIGHT
            elif self.endpoint.endpoint_id == BOTH_BUTTONS_ENDPOINT:
                button=BOTH_BUTTONS
            

            action=action = "{}_{}".format(self.endpoint.endpoint_id, self._current_state)
            if button != None:
                action = "{}_{}".format(button, self._current_state)

            self.listener_event(ZHA_SEND_EVENT, action, event_args)
            super()._update_attribute(0, action)


class XiaomiAqaraWSEUK02(CustomDevice):
    """WS-EUK02 Aqara smart wall switch H1 EU (no neutral, double rocker)"""

    signature = {
        MODELS_INFO: [(LUMI, "lumi.switch.l2aeu1")],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    DeviceTemperature.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    OnOff.cluster_id,
                    MultistateInput.cluster_id,
                    OppleSwitchCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
            2: {
                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,
                    MultistateInput.cluster_id,
                    OppleSwitchCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [],
            },
            LEFT_BUTTON_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInput.cluster_id,
                ],
                OUTPUT_CLUSTERS: [],
            },
            RIGHT_BUTTON_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInput.cluster_id,
                ],
                OUTPUT_CLUSTERS: [],
            },
            BOTH_BUTTONS_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInput.cluster_id,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                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,
                    DeviceTemperature.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    OnOff.cluster_id,
                    Alarms.cluster_id,
                    MultistateInputCluster,
                    XiaomiAqaraMasterCluster,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
            2: {
                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,
                    MultistateInputCluster,
                    XiaomiAqaraSecondaryCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            LEFT_BUTTON_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInputCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            RIGHT_BUTTON_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInputCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            BOTH_BUTTONS_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInputCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

    device_automation_triggers = {
        (SHORT_PRESS, LEFT): { COMMAND: "{}_{}".format(LEFT, COMMAND_SINGLE)},
        (DOUBLE_PRESS, LEFT): { COMMAND: "{}_{}".format(LEFT, COMMAND_DOUBLE)},
        (SHORT_PRESS, RIGHT): { COMMAND: "{}_{}".format(RIGHT, COMMAND_SINGLE)},
        (DOUBLE_PRESS, RIGHT): { COMMAND: "{}_{}".format(RIGHT, COMMAND_DOUBLE)},
        (SHORT_PRESS, BOTH_BUTTONS): { COMMAND: "{}_{}".format(BOTH_BUTTONS, COMMAND_SINGLE)},
        (DOUBLE_PRESS, BOTH_BUTTONS): { COMMAND: "{}_{}".format(BOTH_BUTTONS, COMMAND_DOUBLE)},
        (COMMAND_HOLD, BUTTON): { COMMAND: "{}_{}".format(BUTTON_ANY, COMMAND_HOLD)},
    }

@ardstuebck
Copy link

Hi, unfortunately, neither of the quirks posted here work for my device. I don't receive any events when listening to "zha_event." My device is shown as two lights, and the right button doesn't change when I press the button physically. Can anyone help me?

My device 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: 128>, 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=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": "0x0104",
      "device_type": "0x0100",
      "input_clusters": [
        "0x0000",
        "0x0002",
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0006",
        "0x0009"
      ],
      "output_clusters": [
        "0x000a",
        "0x0019"
      ]
    },
    "2": {
      "profile_id": "0x0104",
      "device_type": "0x0100",
      "input_clusters": [
        "0x0000",
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0006"
      ],
      "output_clusters": []
    },
    "242": {
      "profile_id": "0xa1e0",
      "device_type": "0x0061",
      "input_clusters": [],
      "output_clusters": [
        "0x0021"
      ]
    }
  },
  "manufacturer": "LUMI",
  "model": "lumi.switch.l2aeu1",
  "class": "zigpy.device.Device"
}

@theorlangur
Copy link
Contributor

@sophipl , thanks for the quirk, it works for me (l2aeu1)
for anyone attempting to apply it as-is:
it seems that it is a little bit outdated and there is no 'OPPLE_CLUSTER_ID':

from zhaquirks.xiaomi.aqara.opple_remote import (
    OppleCluster,
    OPPLE_CLUSTER_ID,
    STATUS_TYPE_ATTR,
    PRESS_TYPES
)

my fix was to remove OPPLE_CLUSTER_ID from the imports and replace all usages in this file with OppleCluster.cluster_id

updated version of aqara_l2aeu1.py
import copy
from enum import Enum

from zigpy import types as t
from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
from zhaquirks import CustomCluster
from zigpy.zcl.clusters.general import (
    Basic,
    DeviceTemperature,
    Groups,
    Identify,
    OnOff,
    Ota,
    Scenes,
    Time,
    MultistateInput,
    GreenPowerProxy,
    Alarms,
)

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
    SHORT_PRESS,
    TRIPLE_PRESS,
    LONG_PRESS,
    ALT_DOUBLE_PRESS,
    DOUBLE_PRESS,
    ALT_SHORT_PRESS,
    LEFT,
    RIGHT,
    COMMAND_TOGGLE,
    COMMAND,
    BUTTON,
    ENDPOINT_ID,
    ARGS,
    CLUSTER_ID,
    ATTR_ID,
    PRESS_TYPE,
    VALUE,
    ZHA_SEND_EVENT,
    COMMAND_HOLD,
    COMMAND_TRIPLE
)
from zhaquirks.xiaomi import (
    LUMI
)

COMMAND_SINGLE = "single"
COMMAND_DOUBLE = "double"
BUTTON_ANY = "any"
LEFT_BUTTON_ENDPOINT=41
RIGHT_BUTTON_ENDPOINT=42
BOTH_BUTTONS_ENDPOINT=51

from zhaquirks.xiaomi.aqara.opple_switch import (OppleSwitchCluster, BOTH_BUTTONS)
from zhaquirks.xiaomi.aqara.opple_remote import (
    OppleCluster,
    STATUS_TYPE_ATTR,
    PRESS_TYPES
)


class XiaomiAqaraOpMode(t.uint8_t, Enum):
    Decoupled = 0x00
    Coupled = 0x01

class XiaomiAqaraFlickerMode(t.uint16_t, Enum):
    Anti_flicker = 0x0004
    Quick = 0x0001

class XiaomiAqaraIndicatorLight(t.uint8_t, Enum):
    Normal = 0x00
    Reverse = 0x01

class XiaomiAqaraOperationMode(t.uint8_t, Enum):
    Decoupled = 0x00
    Coupled = 0x01


class XiaomiAqaraMasterCluster(OppleCluster):

    attributes = copy.deepcopy(OppleCluster.attributes)

    attributes.update(
        {
            0x0004: ("anti_flicker", XiaomiAqaraFlickerMode, True),
            0x0002: ("power_outage_count", t.uint8_t, True),
            0x0201: ("power_outage_memory", t.Bool, True),
            0x0203: ("led_disabled_night", t.Bool, True),
            0x00F0: ("indicator_light", XiaomiAqaraIndicatorLight, True),
            0x0200: ("operation_mode", XiaomiAqaraOperationMode, True),
        }
    )

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == 0x00FC and value == False:
            event_args = {
                BUTTON: BUTTON_ANY,
                PRESS_TYPE: COMMAND_HOLD,
                ATTR_ID: attrid,
                VALUE: value,
            }
            action = "{}_{}".format(BUTTON_ANY, COMMAND_HOLD)
            self.listener_event(ZHA_SEND_EVENT, action, event_args)
            super()._update_attribute(0, action)
#        elif attrid == 0x00f7:
#            event_args = {
#                BUTTON: BUTTON_ANY,
#                PRESS_TYPE: COMMAND_TRIPLE,
#                ATTR_ID: attrid
#            }
#            action = "{}_{}".format(BUTTON_ANY, COMMAND_TRIPLE)
#            self.listener_event(ZHA_SEND_EVENT, action, event_args)
#            super()._update_attribute(0, action)


class XiaomiAqaraSecondaryCluster(CustomCluster):
    ep_attribute = "opple_cluster"
    cluster_id = OppleCluster.cluster_id

    attributes = (
        {
            0x0200: ("operation_mode", XiaomiAqaraOperationMode, True), 
        }
    )

class MultistateInputCluster(CustomCluster, MultistateInput):
    cluster_id = MultistateInput.cluster_id

    def __init__(self, *args, **kwargs):
        self._current_state = None
        super().__init__(*args, **kwargs)

    async def configure_reporting(
        self,
        attribute,
        min_interval,
        max_interval,
        reportable_change,
        manufacturer=None,
    ): """Configure reporting."""

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == STATUS_TYPE_ATTR:
            self._current_state = PRESS_TYPES.get(value)
            event_args = {
                BUTTON: self.endpoint.endpoint_id,
                PRESS_TYPE: self._current_state,
                ATTR_ID: attrid,
                VALUE: value,
            }
            button = None

            if self.endpoint.endpoint_id == LEFT_BUTTON_ENDPOINT:
                button=LEFT
            elif self.endpoint.endpoint_id == RIGHT_BUTTON_ENDPOINT:
                button=RIGHT
            elif self.endpoint.endpoint_id == BOTH_BUTTONS_ENDPOINT:
                button=BOTH_BUTTONS
            

            action=action = "{}_{}".format(self.endpoint.endpoint_id, self._current_state)
            if button != None:
                action = "{}_{}".format(button, self._current_state)

            self.listener_event(ZHA_SEND_EVENT, action, event_args)
            super()._update_attribute(0, action)


class XiaomiAqaraWSEUK02(CustomDevice):
    """WS-EUK02 Aqara smart wall switch H1 EU (no neutral, double rocker)"""

    signature = {
        MODELS_INFO: [(LUMI, "lumi.switch.l2aeu1")],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    DeviceTemperature.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    OnOff.cluster_id,
                    Alarms.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
            2: {
                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,
                    MultistateInput.cluster_id,
                    OppleSwitchCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                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,
                    DeviceTemperature.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    OnOff.cluster_id,
                    Alarms.cluster_id,
                    MultistateInputCluster,
                    XiaomiAqaraMasterCluster,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
            2: {
                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,
                    MultistateInputCluster,
                    XiaomiAqaraSecondaryCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            LEFT_BUTTON_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInputCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            RIGHT_BUTTON_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInputCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            BOTH_BUTTONS_ENDPOINT: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    MultistateInputCluster,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

    device_automation_triggers = {
        (SHORT_PRESS, LEFT): { COMMAND: "{}_{}".format(LEFT, COMMAND_SINGLE)},
        (DOUBLE_PRESS, LEFT): { COMMAND: "{}_{}".format(LEFT, COMMAND_DOUBLE)},
        (SHORT_PRESS, RIGHT): { COMMAND: "{}_{}".format(RIGHT, COMMAND_SINGLE)},
        (DOUBLE_PRESS, RIGHT): { COMMAND: "{}_{}".format(RIGHT, COMMAND_DOUBLE)},
        (SHORT_PRESS, BOTH_BUTTONS): { COMMAND: "{}_{}".format(BOTH_BUTTONS, COMMAND_SINGLE)},
        (DOUBLE_PRESS, BOTH_BUTTONS): { COMMAND: "{}_{}".format(BOTH_BUTTONS, COMMAND_DOUBLE)},
        (COMMAND_HOLD, BUTTON): { COMMAND: "{}_{}".format(BUTTON_ANY, COMMAND_HOLD)},
    } 

@TheJulianJES TheJulianJES added the Xiaomi Request/PR regarding a Xiaomi device label Dec 24, 2023
Copy link

There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some of the old issues, as many of them have already been resolved with the latest updates. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by adding a comment 👍 This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions.

@github-actions github-actions bot added the stale Issue is inactivate and might get closed soon label Jun 21, 2024
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jun 28, 2024
@dmpcc
Copy link

dmpcc commented Nov 14, 2024

The quirk provided by theorlang has enabled the OppleSwitchCluster option for me. I can now decouple the switch from the button. I do see some other options now that are not supported by the Switch (e.g. Power sensor). In addition the device pages does sometimes become unresponsive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stale Issue is inactivate and might get closed soon Xiaomi Request/PR regarding a Xiaomi device
Projects
None yet
Development

No branches or pull requests

8 participants