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

[Device Support Request] Aqara H1 Knob (Wireless) - lumi.remote.rkba01 - ZNXNKG02LM #2266

Open
oxc opened this issue Mar 10, 2023 · 31 comments
Labels
Xiaomi Request/PR regarding a Xiaomi device

Comments

@oxc
Copy link

oxc commented Mar 10, 2023

This has been reported as #929 which has been closed as stale. It's still relevant, so I'll try to combine my own information with those from that bug report.

Is your feature request related to a problem? Please describe.
When pairing the device to my Home Assistant ZHA integration using ConBee II, it only exposes LQI and RSSI sensors.

Describe the solution you'd like
Support for: Battery, Dimmer, Press, Double Press, Press and Hold, Press and turn.

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|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": "0x0103",
      "in_clusters": [
        "0x0000",
        "0x0001",
        "0x0003"
      ],
      "out_clusters": [
        "0x0003",
        "0x0006",
        "0x0019"
      ]
    }
  },
  "manufacturer": "LUMI",
  "model": "lumi.remote.rkba01",
  "class": "zigpy.device.Device"
}
Diagnostic information
{
  "home_assistant": {
    "installation_type": "Home Assistant Supervised",
    "version": "2023.3.3",
    "dev": false,
    "hassio": true,
    "virtualenv": false,
    "python_version": "3.10.10",
    "docker": true,
    "arch": "aarch64",
    "timezone": "Europe/Berlin",
    "os_name": "Linux",
    "os_version": "5.15.76-v8+",
    "supervisor": "2023.03.1",
    "host_os": "Debian GNU/Linux 11 (bullseye)",
    "docker_version": "20.10.21",
    "chassis": "",
    "run_as_root": true
  },
  "custom_components": {
    "google_home": {
      "version": "1.9.17",
      "requirements": [
        "glocaltokens==0.7.0"
      ]
    },
    "huesyncbox": {
      "version": "1.23.0",
      "requirements": [
        "aiohuesyncbox==0.0.21"
      ]
    },
    "average": {
      "version": "2.3.0",
      "requirements": []
    },
    "hacs": {
      "version": "1.31.0",
      "requirements": [
        "aiogithubapi>=22.10.1"
      ]
    },
    "iq_notify": {
      "version": "1.0.0",
      "requirements": []
    }
  },
  "integration_manifest": {
    "domain": "zha",
    "name": "Zigbee Home Automation",
    "after_dependencies": [
      "onboarding",
      "usb"
    ],
    "codeowners": [
      "@dmulcahey",
      "@adminiuga",
      "@puddly"
    ],
    "config_flow": true,
    "dependencies": [
      "file_upload"
    ],
    "documentation": "https://www.home-assistant.io/integrations/zha",
    "iot_class": "local_polling",
    "loggers": [
      "aiosqlite",
      "bellows",
      "crccheck",
      "pure_pcapy3",
      "zhaquirks",
      "zigpy",
      "zigpy_deconz",
      "zigpy_xbee",
      "zigpy_zigate",
      "zigpy_znp"
    ],
    "requirements": [
      "bellows==0.34.9",
      "pyserial==3.5",
      "pyserial-asyncio==0.6",
      "zha-quirks==0.0.93",
      "zigpy-deconz==0.19.2",
      "zigpy==0.53.2",
      "zigpy-xbee==0.16.2",
      "zigpy-zigate==0.10.3",
      "zigpy-znp==0.9.3"
    ],
    "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"
        ]
      }
    ],
    "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*"
      }
    ],
    "is_built_in": true
  },
  "data": {
    "ieee": "**REDACTED**",
    "nwk": 36806,
    "manufacturer": "LUMI",
    "model": "lumi.remote.rkba01",
    "name": "LUMI lumi.remote.rkba01",
    "quirk_applied": false,
    "quirk_class": "zigpy.device.Device",
    "manufacturer_code": 4447,
    "power_source": "Mains",
    "lqi": 255,
    "rssi": -52,
    "last_seen": "2023-03-10T11:37:52",
    "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": "0x0103",
          "in_clusters": [
            "0x0000",
            "0x0001",
            "0x0003"
          ],
          "out_clusters": [
            "0x0003",
            "0x0006",
            "0x0019"
          ]
        }
      }
    },
    "active_coordinator": false,
    "entities": [
      {
        "entity_id": "button.lumi_lumi_remote_rkba01_identify",
        "name": "LUMI lumi.remote.rkba01"
      }
    ],
    "neighbors": [],
    "routes": [],
    "endpoint_names": [
      {
        "name": "ON_OFF_LIGHT_SWITCH"
      }
    ],
    "user_given_name": null,
    "device_reg_id": "1a01e6da98077019a2705886bd4c257b",
    "area_id": null,
    "cluster_details": {
      "1": {
        "device_type": {
          "name": "ON_OFF_LIGHT_SWITCH",
          "id": 259
        },
        "profile_id": 260,
        "in_clusters": {
          "0x0000": {
            "endpoint_attribute": "basic",
            "attributes": {
              "0x0004": {
                "attribute_name": "manufacturer",
                "value": "LUMI"
              },
              "0x0005": {
                "attribute_name": "model",
                "value": "lumi.remote.rkba01"
              }
            },
            "unsupported_attributes": {}
          },
          "0x0003": {
            "endpoint_attribute": "identify",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0001": {
            "endpoint_attribute": "power",
            "attributes": {},
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {
          "0x0003": {
            "endpoint_attribute": "identify",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0019": {
            "endpoint_attribute": "ota",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0006": {
            "endpoint_attribute": "on_off",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      }
    }
  }
}
Additional logs

The following actions trigger a zha_event:

Long Press:

event_type: zha_event
data:
  device_ieee: 54:ef:44:10:00:7b:c2:b9
  unique_id: 54:ef:44:10:00:7b:c2:b9:1:0x0006
  device_id: 1a01e6da98077019a2705886bd4c257b
  endpoint_id: 1
  cluster_id: 6
  command: "off"
  args: []
  params: {}
origin: LOCAL
time_fired: "2023-03-10T10:49:59.232221+00:00"
context:
  id: 01GV5J8K60QF3P265DVBWWPS05
  parent_id: null
  user_id: null

Double Press:

event_type: zha_event
data:
  device_ieee: 54:ef:44:10:00:7b:c2:b9
  unique_id: 54:ef:44:10:00:7b:c2:b9:1:0x0006
  device_id: 1a01e6da98077019a2705886bd4c257b
  endpoint_id: 1
  cluster_id: 6
  command: toggle
  args: []
  params: {}
origin: LOCAL
time_fired: "2023-03-10T10:50:38.945507+00:00"
context:
  id: 01GV5J9SZ1ES9ZV2JSP9NS5K6Z
  parent_id: null
  user_id: null

Additional context
Long Press and Double Press yield a zha_event.
Single Press, as well as Turning the Knob yields no events.

Zigbee2MQTT implementation

https://github.com/Koenkk/zigbee-herdsman-converters/blob/b06b48efee3df32a87bfa1b7f6fb2f00bda7aa15/devices/xiaomi.js#LL2934-L2956

    {
        zigbeeModel: ['lumi.remote.rkba01'],
        model: 'ZNXNKG02LM',
        vendor: 'Xiaomi',
        description: 'Aqara knob H1 (wireless)',
        meta: {battery: {voltageToPercentage: '3V_2850_3000'}},
        exposes: [e.battery(), e.battery_voltage(),
            e.action(['single', 'double', 'hold', 'release', 'start_rotating', 'rotation', 'stop_rotating']),
            exposes.enum('operation_mode', ea.ALL, ['event', 'command']).withDescription('Button mode'),
            exposes.numeric('action_rotation_angle', ea.STATE).withUnit('*').withDescription('Rotation angle'),
            exposes.numeric('action_rotation_angle_speed', ea.STATE).withUnit('*').withDescription('Rotation angle speed'),
            exposes.numeric('action_rotation_percent', ea.STATE).withUnit('%').withDescription('Rotation percent'),
            exposes.numeric('action_rotation_percent_speed', ea.STATE).withUnit('%').withDescription('Rotation percent speed'),
            exposes.numeric('action_rotation_time', ea.STATE).withUnit('ms').withDescription('Rotation time'),
        ],
        fromZigbee: [fz.xiaomi_on_off_action, fz.xiaomi_multistate_action, fz.xiaomi_basic, fz.aqara_opple, fz.aqara_knob_rotation],
        toZigbee: [tz.aqara_opple_operation_mode],
        onEvent: preventReset,
        configure: async (device, coordinatorEndpoint, logger) => {
            const endpoint1 = device.getEndpoint(1);
            await endpoint1.write('aqaraOpple', {'mode': 1}, {manufacturerCode: 0x115f, disableResponse: true});
        },
    },
@oxc oxc changed the title [Device Support Request] Aqara H1 Knob - lumi.remote.rkba01 - ZNXNKG02LM [Device Support Request] Aqara H1 Knob (Wireless) - lumi.remote.rkba01 - ZNXNKG02LM Mar 10, 2023
@oxc
Copy link
Author

oxc commented Mar 18, 2023

Attached is my custom quirk

What works:

  • Rotate (start_rotation, rotation, stop_rotation) (requires replacement endpoint 71)
  • Hold and rotate (hold_start_rotation, hold_rotation, stop_hold_rotation) (requires replacement endpoint 72)
  • Single press (1_single)
  • Double press (1_double)
  • Hold and release (1_hold, 1_release)
  • Battery (requires replacement node descriptor, because the original one lists MainsPowered)

What doesn't work:

  • I can't figure out how to change the sensitivity of the rotation (I assume it only affects the percent value, where this is relevant because it is capped to 100). There's probably some attribute that can be set, either on endpoint 1 or maybe on 71/72, but this is just me guessing.
  • In command mode, I'm not able to trigger anything on single press. Can someone explain what this mode is for anyway?

Other thoughts:

  • I'm not sure if the rotate_left/rotate_right events make sense. Is there a way to trigger a device automation trigger based on "greater than"/"less than"? Would it make more sense to add a "direction" event arg and use that to define the device_automation_trigger on stop_rotation?
  • I've copied the event names from zigbee-herdsman-converters, but renamed the "_speed" suffix to "_delta". This seems more appropriate to me, the delta attributes are the changes done to the respective attribute value in this rotation event, whereas the unsuffixed attriute is the accumulated change since the start_rotation event.
  • The battery on my new device reports 100% and 3.2V, which seems a bit high. But maybe it goes down to 3V quickly.
Source code: xiamo/aqara/remote_h1_knob.py
from typing import Optional, Union, List, Any

import zigpy.types as t
from zigpy.profiles import zha
from zigpy.zcl import foundation
from zigpy.zcl.clusters.general import Basic, Identify, OnOff, \
    PowerConfiguration, Ota
from zigpy.zdo.types import NodeDescriptor, LogicalType

from zhaquirks.const import (
    ALT_DOUBLE_PRESS,
    ARGS,
    BUTTON,
    COMMAND,
    COMMAND_OFF,
    COMMAND_TOGGLE,
    DEVICE_TYPE,
    DOUBLE_PRESS,
    ENDPOINT_ID,
    ENDPOINTS,
    INPUT_CLUSTERS,
    LONG_PRESS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
    SHORT_PRESS,
    LONG_RELEASE,
    ZHA_SEND_EVENT, ALT_LONG_PRESS, NODE_DESCRIPTOR,
)
from zhaquirks.xiaomi import (
    LUMI,
    BasicCluster,
    XiaomiAqaraE1Cluster,
    XiaomiCustomDevice,
)
from zhaquirks.xiaomi.aqara.opple_remote import (
    COMMAND_1_DOUBLE,
    COMMAND_1_HOLD,
    COMMAND_1_SINGLE,
    MultistateInputCluster, COMMAND_1_RELEASE,
)
from zhaquirks.xiaomi.aqara.remote_h1 import PowerConfigurationClusterH1Remote

START_ROTATION = "start_rotation"
ROTATION = "rotation"
STOP_ROTATION = "stop_rotation"
HOLD_START_ROTATION = "hold_start_rotation"
HOLD_ROTATION = "hold_rotation"
HOLD_STOP_ROTATION = "hold_stop_rotation"

ROTATE_RIGHT = "rotate_right"
ROTATE_LEFT = "rotate_left"
HOLD_ROTATE_LEFT = "hold_rotate_left"
HOLD_ROTATE_RIGHT = "hold_rotate_right"

class KnobAction(t.enum8):
    """Knob action mode enum."""

    off = 0x00
    start_rotation = 0x01
    rotation = 0x02
    stop_rotation = 0x03
    hold_start_rotation = 0x81
    hold_rotation = 0x82
    hold_stop_rotation = 0x83


class AqaraRemoteManuSpecificCluster(XiaomiAqaraE1Cluster):
    """Aqara manufacturer specific settings."""

    ep_attribute = "aqara_cluster"

    # manufacture override code: 4447 (0x115f)
    # to get/set this attribute, you might need to click the button 5 times
    # quickly.
    attributes = XiaomiAqaraE1Cluster.attributes.copy()
    attributes.update(
        {
            # operation_mode:
            # 0 means "command" mode.
            # 1 means "event" mode.
            0x0009: ("operation_mode", t.uint8_t, True),
        }
    )

class KnobManuSpecificCluster(XiaomiAqaraE1Cluster):
    """Aqara manufacturer specific settings."""

    ep_attribute = "aqara_cluster"

    attributes = XiaomiAqaraE1Cluster.attributes.copy()
    attributes.update(
        {
            0x022C: ("rotation_time_delta", t.uint16_t, True),
            0x0231: ("rotation_time", t.uint32_t, True),
            #0x0238: ("unknown_0238", t.uint8_t, True), # always value=12,
            0x0230: ("rotation_angle_delta", t.Single, True),
            0x022E: ("rotation_angle", t.Single, True),
            0x0232: ("rotation_percent_delta", t.Single, True),
            0x0233: ("rotation_percent", t.Single, True),
            0x023A: ("action", KnobAction, True),
        }
    )

    def handle_cluster_general_request(
        self,
        header: foundation.ZCLHeader,
        args: List[Any],
        *,
        dst_addressing: Optional[
            Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK]
        ] = None,
    ):
        """Handle the cluster command."""
        self.info(
            "H1 knob general request - handle_cluster_general_request: header: %s - args: [%s]",
            header,
            args,
        )

        super().handle_cluster_general_request(header, args, dst_addressing=dst_addressing)

        if header.command_id != foundation.GeneralCommand.Report_Attributes:
            return

        event_args = {}
        for attr in args.attribute_reports:
            if attr.attrid in self.attributes:
                attr_name = self.attributes[attr.attrid].name
                try:
                    value = self.attributes[attr.attrid].type(attr.value.value)
                except ValueError:
                    self.debug(
                        "Couldn't normalize %s attribute with %s value",
                        attr_name,
                        attr.value.value,
                        exc_info=True,
                    )
                    value = attr.value.value
                event_args[attr_name] = value

        action = event_args.get("action")

        if not action:
            return

        command = action.name

        is_stop_action = action in (
            KnobAction.stop_rotation, KnobAction.hold_stop_rotation
        )

        # delta attributes are outdated (or 0) on stop_rotation, don't send them
        if is_stop_action:
            for attr in list(event_args.keys()):
                if attr.endswith("_delta"):
                    del event_args[attr]

        self.listener_event(ZHA_SEND_EVENT, command, event_args)

        if is_stop_action:
            if event_args.get("rotation_angle") > 0:
                command = HOLD_ROTATE_RIGHT if action == KnobAction.hold_stop_rotation else ROTATE_RIGHT
                self.listener_event(ZHA_SEND_EVENT, command, event_args)
            elif event_args.get("rotation_angle") < 0:
                command = HOLD_ROTATE_LEFT if action == KnobAction.hold_stop_rotation else ROTATE_LEFT
                self.listener_event(ZHA_SEND_EVENT, command, event_args)

class AqaraH1KnobWireless(XiaomiCustomDevice):
    """Aqara H1 Knob (Wireless)"""
    signature = {
        # 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)
        MODELS_INFO: [(LUMI, "lumi.remote.rkba01")],
        ENDPOINTS: {
            # "1": {
            #     "profile_id": 260,
            #     "device_type": "0x0103",
            #     "in_clusters": ["0x0000","0x0001","0x0003"],
            #     "out_clusters": ["0x0003","0x0006","0x0019"]
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    PowerConfiguration.cluster_id,
                    Identify.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Identify.cluster_id,
                    OnOff.cluster_id,
                    Ota.cluster_id,
                ],
            },
        },
    }
    replacement = {
        # use custom NodeDescriptor to remove MainsPowered flag
        NODE_DESCRIPTOR: NodeDescriptor(
            logical_type=LogicalType.EndDevice,
            complex_descriptor_available=0,
            user_descriptor_available=0,
            reserved=0,
            aps_flags=0,
            frequency_band=NodeDescriptor.FrequencyBand.Freq2400MHz,
            mac_capability_flags=NodeDescriptor.MACCapabilityFlags.AllocateAddress, # not MainsPowered,
            manufacturer_code=4447,
            maximum_buffer_size=127,
            maximum_incoming_transfer_size=100,
            server_mask=11264,
            maximum_outgoing_transfer_size=100,
            descriptor_capability_field=NodeDescriptor.DescriptorCapability.NONE,
        ),
        ENDPOINTS: {
            1: {
                INPUT_CLUSTERS: [
                    BasicCluster,
                    Identify.cluster_id,
                    PowerConfigurationClusterH1Remote,
                    MultistateInputCluster,
                    AqaraRemoteManuSpecificCluster,
                ],
                OUTPUT_CLUSTERS: [
                    Identify.cluster_id,
                    OnOff.cluster_id,
                    Ota.cluster_id,
                ],
            },
            71: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.DIMMER_SWITCH,
                INPUT_CLUSTERS: [
                    KnobManuSpecificCluster,
                ],
            },
            72: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SHADE_CONTROLLER,
                INPUT_CLUSTERS: [
                    KnobManuSpecificCluster,
                ],
            }
        },
    }

    device_automation_triggers = {
        # triggers when operation_mode == event
        (SHORT_PRESS, BUTTON): {COMMAND: COMMAND_1_SINGLE},
        (DOUBLE_PRESS, BUTTON): {COMMAND: COMMAND_1_DOUBLE},
        (LONG_PRESS, BUTTON): {COMMAND: COMMAND_1_HOLD},
        (LONG_RELEASE, BUTTON): {COMMAND: COMMAND_1_RELEASE},
        (ROTATE_LEFT, BUTTON): {COMMAND: ROTATE_LEFT},
        (ROTATE_RIGHT, BUTTON): {COMMAND: ROTATE_RIGHT},
        (HOLD_ROTATE_LEFT, BUTTON): {COMMAND: HOLD_ROTATE_LEFT},
        (HOLD_ROTATE_RIGHT, BUTTON): {COMMAND: HOLD_ROTATE_RIGHT},
        # triggers when operation_mode == command
        ## single press does not trigger anything
        (ALT_DOUBLE_PRESS, BUTTON): {COMMAND: COMMAND_TOGGLE, ENDPOINT_ID: 1, ARGS: []},
        (ALT_LONG_PRESS, BUTTON): {COMMAND: COMMAND_OFF, ENDPOINT_ID: 1, ARGS: []},
    }

@oxc
Copy link
Author

oxc commented Mar 24, 2023

I've put the code into a gist to track the changes for now: https://gist.github.com/oxc/754d6436ce62d92af660d3671acd9346

I decided that I don't like the separate events for rotate_left/right, and added a rotation_direction arg. This feels more appropriate.

If I wanted to create a PR for this, what else would be needed?

@nwithan8
Copy link

nwithan8 commented Mar 24, 2023

Just got mine today, good to know someone is working on this (as recently as 7 hours ago, no less!), since I have no idea how to.

EDIT: Quirk works great! One thing I noticed, the direction is determined based on the net gain in either direction. For example, turning the dial +1 click to the left and then +4 clicks to the right in one motion is a +3 net rotation right, so is considered "rotate right", even though I started by rotating left. If I rotate left +4 and then right +1, it's a +3 net rotation left, and that's considered "rotate left".

@oxc
Copy link
Author

oxc commented Mar 24, 2023

One thing I noticed, the direction is determined based on the net gain in either direction. For example, turning the dial +1 click to the left and then +4 clicks to the right in one motion is a +3 net rotation right, so is considered "rotate right", even though I started by rotating left. If I rotate left +4 and then right +1, it's a +3 net rotation left, and that's considered "rotate left".

That's correct. This is something that's handled internally by the device itself.

I thought that it sends a stop_rotation and a new start_rotation when the direction changes, but in my latest test (with a different Knob than the one I initially developed the quirk with), this was indeed not the case.

It's even funnier if you compare percent to angle: when you do two full turns to the left, you will be at -720deg but only at -100%, because that is capped at 100. If you now do 1.5 turns to the right, you will be at -180deg but +50% :) That's why I thought it would be nice to be able to change the sensitivity, but I'm no longer even sure if that's indeed controlled in the device, or just in the Aqara software that implements this quirk (probably the Hub, I guess?).

@nwithan8
Copy link

In the meantime, was able to crank out a few blueprints for the device:
https://community.home-assistant.io/t/aqara-h1-rotary-dimmer-switch-dial-remote/551909
https://community.home-assistant.io/t/aqara-h1-rotary-dimmer-switch-dial-media-controls/551917

Thanks @oxc for the great work, and I vouch that support for this should get adopted ASAP

@sleepy-salmon
Copy link

@oxc Thank you so much for your work on this. I think that I am having trouble because I am stuck in command mode but I've not been able to get it into event mode.
Do you have any advice on how to achieve this? What I've tried so far is to go on manage zigbee device, do the quick 5 button press and then read attribute, but this fails. I try inputting the value as 1 and the manufacturer override code and writting the attribute, this gives me a tick but the operation of the device doesn't change. How I am I supposed to go about this correctly?

image

@sleepy-salmon
Copy link

My problem solved itself, it just decided to allow me to change the operation mode one day and now I've got it into event mode.

Thanks again for your quirk!

@oxc
Copy link
Author

oxc commented May 17, 2023

Sorry I didn't reply before. I don't remember how I did it exactly, I believe I pressed 5 times afterwards instead of before. IIRC, I had to press at least once before the commands would return. But I'm really not sure how exactly I didn't it 😂

@0rangutan
Copy link

@oxc - many thanks for supplying this.
I have the quirk successfully applied according to the H1 knob's Device Zigbee info, but it still only shows Battery, Identify, LQI and RSSI in ZHA, no other capabilities. I have restarted HA and removed/re-added the device countless times.
Are there any other steps that I need to complete?
Many thanks

@oxc
Copy link
Author

oxc commented Jun 2, 2023

@0rangutan, if the quirk loaded correctly, you can now configure triggers for the device, and will also receive zha_events. Also have a look at the blueprints from @nwithan8 in the comment above.

@0rangutan
Copy link

Thanks - no zhe_events are firing and @nwithan8's blueprints are set up but not working.
Any ideas?

@karrui
Copy link

karrui commented Aug 10, 2023

Just got this knob too. Quirk works great for events. Cannot figure out how to continuously dim a light as the knob is turned though. Is that possible? Setting Decrease <light> brightness on turn event just reduces it by a static 10%, and only after the knob has stopped turning (and thus only when the Rotated event fires). Am I missing something?

@oxc
Copy link
Author

oxc commented Aug 10, 2023

You will have to use the "rotation" zha event for that.

@karrui
Copy link

karrui commented Aug 10, 2023

You will have to use the "rotation" zha event for that.

Thanks for the quick reply! Good to know it was user error. Will experiment more.

@oxc
Copy link
Author

oxc commented Aug 10, 2023

Perhaps it makes sense to expose the ongoing rotation as events too. I would have appreciated some input from the maintainers on that, but that seems hard to come by.

@nwithan8
Copy link

nwithan8 commented Aug 10, 2023

Thanks - no zhe_events are firing and @nwithan8's blueprints are set up but not working. Any ideas?

Hey, just noticed today that the some of the commands are reporting differently. Specifically, hold and double are now different (will update the blueprint accordingly), but single press isn't registering at all with ZHA (device lights up, so I know something got triggered).

EDIT: I think I figured it out, device got set into a different mode. Potentially the same issue for you @0rangutan

@oxc offhand, do you know how to toggle between the two modes?

https://gist.github.com/oxc/754d6436ce62d92af660d3671acd9346#file-remote_h1_knob-py-L253

EDIT 2: Code comments help, lol: https://gist.github.com/oxc/754d6436ce62d92af660d3671acd9346#file-remote_h1_knob-py-L77

Unfortunately, can't get my devices out of the alt mode. I have three of them and even the ones I haven't touched in forever are now in this alt mode. I'm starting to suspect a firmware update...

@nwithan8
Copy link

Thanks - no zhe_events are firing and @nwithan8's blueprints are set up but not working. Any ideas?

Hey, just noticed today that the some of the commands are reporting differently. Specifically, hold and double are now different (will update the blueprint accordingly), but single press isn't registering at all with ZHA (device lights up, so I know something got triggered).

EDIT: I think I figured it out, device got set into a different mode. Potentially the same issue for you @0rangutan

@oxc offhand, do you know how to toggle between the two modes?

https://gist.github.com/oxc/754d6436ce62d92af660d3671acd9346#file-remote_h1_knob-py-L253

EDIT 2: Code comments help, lol: https://gist.github.com/oxc/754d6436ce62d92af660d3671acd9346#file-remote_h1_knob-py-L77

Unfortunately, can't get my devices out of the alt mode. I have three of them and even the ones I haven't touched in forever are now in this alt mode. I'm starting to suspect a firmware update...

In the meantime, I have made COMMAND mode variants of the blueprints @0rangutan (see "Edit" section of each original post):

https://community.home-assistant.io/t/aqara-h1-rotary-dimmer-switch-dial-remote/551909
https://community.home-assistant.io/t/aqara-h1-rotary-dimmer-switch-dial-media-controls/551917

@oxc
Copy link
Author

oxc commented Aug 10, 2023

I believe you have to set the operation_mode attribute and then press 5 times. Or the other way around. I cannot say for sure, but somehow like that I managed to switch. The same is reported for some other Aqara devices, and at least back then I could not find a definitive guide on how to do it.

@karrui
Copy link

karrui commented Aug 11, 2023

Can confirm. To set attributes, click "Write" then press the device 5 times quickly and it should go through.

@nwithan8
Copy link

Can confirm. To set attributes, click "Write" then press the device 5 times quickly and it should go through.

Click "Write" where? Is this a Dev Tools service I'm calling, or on the device page?

@karrui
Copy link

karrui commented Aug 11, 2023

Click "Write" where? Is this a Dev Tools service I'm calling, or on the device page?

#2251 (comment) should help

@0rangutan
Copy link

@oxc and @nwithan8 - Thanks for all your efforts on this.
I have this working now in ZHA and my dimmer set to Event mode successfully.
It works well, but with the one big difference vs native integration in Z2M - the ability to dim on the fly as you rotate the knob, as discussed above.
Looking at how this works in Z2M, the rotation % is exposed as a sensor and then dimming can work dynamically as the knob is rotated based on this sensor.
Is there any way of exposing the rotation % as a sensor in this quirk?

This was the code I was using for dimming under Z2M:

alias: Study Lights Dimming
description: ""
trigger:
  - platform: state
    entity_id:
      - sensor.study_rotary_dimmer_action_rotation_percent
    from: unknown
condition: []
action:
  - service: light.turn_on
    data_template:
      brightness: >-
        {{((state_attr('light.study_lights','brightness')|int(1))+((trigger.to_state.state|float(2))/2.55)|round(2)*3)|int(1)}}
    target:
      entity_id: light.study_lights
    enabled: true
mode: single

@nwithan8
Copy link

nwithan8 commented Aug 17, 2023

@oxc and @nwithan8 - Thanks for all your efforts on this. I have this working now in ZHA and my dimmer set to Event mode successfully. It works well, but with the one big difference vs native integration in Z2M - the ability to dim on the fly as you rotate the knob, as discussed above. Looking at how this works in Z2M, the rotation % is exposed as a sensor and then dimming can work dynamically as the knob is rotated based on this sensor. Is there any way of exposing the rotation % as a sensor in this quirk?

This was the code I was using for dimming under Z2M:

alias: Study Lights Dimming
description: ""
trigger:
  - platform: state
    entity_id:
      - sensor.study_rotary_dimmer_action_rotation_percent
    from: unknown
condition: []
action:
  - service: light.turn_on
    data_template:
      brightness: >-
        {{((state_attr('light.study_lights','brightness')|int(1))+((trigger.to_state.state|float(2))/2.55)|round(2)*3)|int(1)}}
    target:
      entity_id: light.study_lights
    enabled: true
mode: single

I'm seeing rotation_percent and rotation_percent_delta exposed in my ZHA event (granted, I'm still in COMMAND mode; couldn't get the attributes updated 😦 )

image

@oxc
Copy link
Author

oxc commented Aug 17, 2023

Yes, they are exposed as zha event properties. Exposing them a sensor would mean having to keep an internal state of the sensor value, which is weird because of the behavior mentioned above.
It should be easy to fill a custom state helper with the desired values based on the ZHA events, though.

@0rangutan
Copy link

Many thanks.

I can see the start_rotation, rotation and stop_rotation events as zha_events, eg.

event_type: zha_event
data:
  device_ieee: 54:ef:44:10:00:7f:62:23
  unique_id: 54:ef:44:10:00:7f:62:23:71:0xfcc0
  device_id: 5e0a167c93599c08f8fbd5798471976a
  endpoint_id: 71
  cluster_id: 64704
  command: rotation
  args:
    rotation_time_delta: 500
    rotation_time: 2200
    rotation_angle_delta: 24
    rotation_angle: 228
    rotation_percent_delta: 6.666666507720947
    rotation_percent: 63.33333206176758
    action: 2
  params: {}
origin: LOCAL
time_fired: "2023-08-18T07:37:07.238438+00:00"
context:
  id: 01H83S75D6F2FV0X1YQV5T3HDJ
  parent_id: null
  user_id: null

So the data is available :)

I'd like to try to create an automation that adjusts the brightness on the fly as the rotation changes. This should work in theory as there is a stream of events that come in as the knob rotates and the various rotation arguments are available.

I'm definitely beyond my level of expertise here. Any ideas how I might trigger based on these events? I have started with the following but this doesn't trigger in the tracing when I rotate the knob.

alias: Study Lights Dimming
description: ""
trigger:
  - platform: event
    event_type: zha_event
    event_data:
      device_ieee: 54:ef:44:10:00:7f:62:23
      endpoint_id: 71
      command: rotation
      args:
        - rotation_percent 

Any ideas?

@nwithan8
Copy link

Many thanks.

I can see the start_rotation, rotation and stop_rotation events as zha_events, eg.

event_type: zha_event
data:
  device_ieee: 54:ef:44:10:00:7f:62:23
  unique_id: 54:ef:44:10:00:7f:62:23:71:0xfcc0
  device_id: 5e0a167c93599c08f8fbd5798471976a
  endpoint_id: 71
  cluster_id: 64704
  command: rotation
  args:
    rotation_time_delta: 500
    rotation_time: 2200
    rotation_angle_delta: 24
    rotation_angle: 228
    rotation_percent_delta: 6.666666507720947
    rotation_percent: 63.33333206176758
    action: 2
  params: {}
origin: LOCAL
time_fired: "2023-08-18T07:37:07.238438+00:00"
context:
  id: 01H83S75D6F2FV0X1YQV5T3HDJ
  parent_id: null
  user_id: null

So the data is available :)

I'd like to try to create an automation that adjusts the brightness on the fly as the rotation changes. This should work in theory as there is a stream of events that come in as the knob rotates and the various rotation arguments are available.

I'm definitely beyond my level of expertise here. Any ideas how I might trigger based on these events? I have started with the following but this doesn't trigger in the tracing when I rotate the knob.

alias: Study Lights Dimming
description: ""
trigger:
  - platform: event
    event_type: zha_event
    event_data:
      device_ieee: 54:ef:44:10:00:7f:62:23
      endpoint_id: 71
      command: rotation
      args:
        - rotation_percent 

Any ideas?

I belive the arg you have there is misconfigured.

You can look at a blueprint I made to trigger and handle the ongoing rotation of another ZHA dial device for maybe reference: https://github.com/nwithan8/configs/blob/9ab215f54a3d6ed0e42de6ddc5f57cd132a3a243/home_assistant/blueprints/automations/ers_rotary_dial_light_control_zha.yaml#L158

That said, I don't recall this device consistently sending our rotation events. I remember it being only one per rotation (you'd have to stop and start a new rotation), but I could be wrong.

@oxc
Copy link
Author

oxc commented Aug 19, 2023

That said, I don't recall this device consistently sending our rotation events. I remember it being only one per rotation (you'd have to stop and start a new rotation), but I could be wrong.

It does for me, and since there are two different events for "percent in this piece of the rotation" rotation_delta and "percent in the full rotation" rotation, I strongly assume that's how it works for everybody (unless yours/its firmware does not report these properties).

@oxc
Copy link
Author

oxc commented Aug 19, 2023

Here is a (slightly redacted) Automation I'm using in production, it controls the light when rotating (uses ongoing rotation), and a curtain when hold+rotating (no ongoing rotation)

Automation YAML for controlling a light on rotation
alias: Knob
description: ""
trigger:
  - platform: event
    event_type: zha_event
    event_data:
      device_id: xxxxxxxxxx
condition: []
action:
  - variables:
      command: "{{ trigger.event.data.command }}"
  - choose:
      - conditions:
          - alias: Hold and rotate
            condition: template
            value_template: |
              {{ command == "hold_stop_rotation" }}
        sequence:
          - variables:
              percent: "{{ trigger.event.data.args.rotation_percent | int(0) }}"
              current_position: |
                {{ state_attr('cover.cover', 'current_position') }}
              target_position: |
                {{ min(max(current_position - percent*2, 0), 100) }}
          - service: cover.set_cover_position
            data:
              position: "{{ target_position }}"
            target:
              entity_id: cover.cover
      - conditions:
          - alias: Rotate
            condition: template
            value_template: >
              {{ command == "start_rotation" or command == "rotation" or command
              == "stop_rotation" }}
        sequence:
          - variables:
              percent_delta: "{{ trigger.event.data.args.rotation_percent_delta | int(0) }}"
          - service: light.turn_on
            data:
              brightness_step_pct: "{{ percent_delta }}"
            target:
              entity_id:
                - light.light1
                - light.light2
      - conditions:
          - alias: Single press
            condition: template
            value_template: |
              {{ command == "1_single" }}
        sequence:
          - if:
              - condition: or
                conditions:
                  - condition: state
                    entity_id: light.light1
                    state: "on"
                  - condition: state
                    entity_id: light.light2
                    state: "on"
            then:
              - service: light.turn_off
                data: {}
                target:
                  entity_id:
                    - light.light1
                    - light.light2
            else:
              - service: light.turn_on
                data: {}
                target:
                  entity_id:
                    - light.light1
                    - light.light2
mode: queued
max: 10

@ToracX
Copy link

ToracX commented Oct 27, 2023

First off thanks @oxc for making the automation yaml it works very well.
Except for single press.
Im having trouble changing operating modes of my H1 dimmer. Does anyone know how to check what mode it is in now?
Also trying te write attributes im getting this error.
Failed to set attribute: value: EVENT attribute: 9 cluster_id: 64704 endpoint_id: 1

Do you put anything in the "value" field? I tried basically everything. event, EVENT, command, COMMAND, 0 ,1 etc...

@arthpeps
Copy link

Hello,

I can't even get the custom quirk working. I'v done following :

  • I added the following lines to my configuration.yaml :

zha:
custom_quirks_path: /config/custom_zha_quirks/
database_path: /config/zigbee.db
enable_quirks: true

  • I created a file named lumi.remote.rkba01.py into the folder custom_zha_quirks with the content of the quirk

I can add the H1 device but interaction goes only from ZHA to the device, not the opposite.

Can anyone help ?

Thanks

arthpeps

@ToracX
Copy link

ToracX commented Dec 23, 2023

Hello,

I can't even get the custom quirk working. I'v done following :

  • I added the following lines to my configuration.yaml :

zha: custom_quirks_path: /config/custom_zha_quirks/ database_path: /config/zigbee.db enable_quirks: true

  • I created a file named lumi.remote.rkba01.py into the folder custom_zha_quirks with the content of the quirk

I can add the H1 device but interaction goes only from ZHA to the device, not the opposite.

Can anyone help ?

Thanks

arthpeps

It is pretty likely you added a space in the .py file when copy pasting everything. You could go into system logs filter zha and post the output here?

@TheJulianJES TheJulianJES added the Xiaomi Request/PR regarding a Xiaomi device label Dec 24, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Xiaomi Request/PR regarding a Xiaomi device
Projects
None yet
Development

No branches or pull requests

8 participants