Foreign Zigbee devices in ZHA: three cases with custom quirks

troubleshooting · smart home published 09/2025

Not every Zigbee device speaks to ZHA the way it should. Cheap Tuya sensors and valves answer in their own dialect, IKEA buttons carry their own clusters. The answer is a custom quirk – a zigpy device handler that tells ZHA how to read and configure the device. The three cases below share one core: the device shows up in ZHA, but without a quirk, or with the wrong one, it does not do what it should. Each carries the date it happened.

mmWave presence sensor MTD085-ZB (July 2026)

A Wenzhi / LeapMMW MTD085-ZB presence sensor (TS0225, _TZ321C_fkzihax8). Goal: a fully working ZHA custom quirk that detects presence, lets every known Tuya parameter be set straight from Home Assistant, matches Zigbee2MQTT behaviour as closely as possible and uses no workarounds or fake defaults.

Starting point

Only IAS motion detection (binary_sensor) worked, through its own polling. No configuration entities, no Tuya DPs, no config read, no Z2M magic packet and no DP reporting back.

What was found

  • IAS part: the device answers the IAS Zone cluster 0x0500, attribute 0x0002 correctly, but it does not send reliable IAS reports on its own. Solved with internal polling in the quirk.
  • Tuya EF00: query_data, setData and sendData failed at first. The problem was not the DP map but the initialisation.
  • Z2M magic packet: on configure, Zigbee2MQTT reads manufacturer, zcl_version, app_version, model, power_source and 0xfffe from the Basic cluster and only then continues configuring. The same was added to the Basic cluster.
  • Basic init: writing 0xffde = 13 and manufacturer 0x1141 is safe and was added.
  • sendData – the breakthrough: after Basic init and the Z2M magic read, TUYA_SEND_DATA started working with no further hacks. The write was verified on DP114 (LED indication) – the LED really reacts. That confirmed the path Home Assistant → quirk → sendData → MCU.

DP map and entities

The DP map was carried over in full from Tuya Cloud. Added entities:

  • Number: sensitivity_trig, distance_trig, disap_time, confirm_time, blockade_time, main_sensitivity, near_distance, far_distance.
  • Sensor: illuminance, debug_distance, debug_cntdown, Cline, target_distance.
  • Select: debug_mode, scene_mode, sensor_ctrl, led_sta, device_sta, range_report_mode.

Not added: presence_state and device_type – their DP is unknown.

What had to be fixed

  • Move to TuyaQuirkBuilder V2: the original V1 approach hit newer ZHA. Newer zigpy requires translation_key, without which the builder refuses to create entities.
  • device_class() was used wrongly and raised an AssertionError in CustomDeviceV2. Removed.
  • fallback_name: generic names (distance_trig) replaced with readable ones (Range – trigger distance).
  • Fake defaults: after a restart the sensor showed 50 %, 1.0 m, 0.3 m even though the device had sent nothing. Removed. New rule: if the device sent nothing, the value is unknown or comes from the HA cache.
  • query_data returned UNSUP_CLUSTER_COMMAND – the firmware does not support it. Removed from production.

RCA

The logs show OUT sendData going out correctly (DP101, DP102, DP104, DP105, DP108, DP111, DP112, DP114, DP115, DP116, DP117, DP120). Not a single IN DP report was found, though. Writing works, reading back does not – for most DPs the sensor behaves as write-only.

Production setup

Keep IAS polling, Basic init, the Z2M magic read and Tuya sendData; no query_data, no fake values; Select entities for enum DPs and clearer names.

Open directions: find out how the Tuya Gateway reads the configuration (sniff the Gateway ↔ sensor traffic), compare with Zigbee2MQTT, add presence_state and device_type once their DP is known, and check whether the firmware sends DP reports under other conditions (announce, restart, pairing).

MOES TV02 thermostatic valve (25 September 2025)

Three identical MOES TV02 valves (TS0601 / _TZE200_kds0pmmv) in ZHA. Goal: get pairing working and fix the non-working Child lock / Boost through the entities the custom quirk creates.

Setup and findings

  • All the TRVs use the quirk ts0601_trv.ZonnsmartTV01_ZG; ZHA shows it as a Quirk. It creates three generic switches: spinac_1 → Boost, spinac_2 → Child lock, spinac_3 → Online mode.
  • Tuya manufacturer-specific communication needs cluster 0xEF00 and manufacturer 0x1002 (4098 dec). The quirk addresses functions by a composite command_id (the high byte is the data type, the low byte the Tuya DP): Child lock 296 (0x0128, DP 40), Boost 613 (0x0265, DP 101), Online 371 (0x0173, DP 115).

The main problem

On one valve, Child lock and Boost both worked through the standard ZHA switch entities. On the other two – same device, same quirk, practically the same settings – the switches controlled nothing. Yet a manual command through ZONNSMARTManufCluster / 0xEF00 worked. That ruled out a bad radio link and the Child lock / Boost function in the TRV itself. Debug on Child lock showed:

set_data(... command_id=296, function=0, data=1)
...
Default_Response(... status=SUCCESS)
...
get_data(... command_id=296 ... data=[1,1])

So the TRV received the command and reported the state back.

Online mode was handled through online_set (0x0173), online (0x0473) and an EP3 switch. Once unified, kitchen and bedroom were set identically (online = 1, EP3 ON), but the matching diagnostic values did not explain why the ZHA switch worked only on some units.

The turning point

On one of the non-working units: Reconfigure → remove device → re-pair. Home Assistant recognised the device by the same IEEE and restored the same entities and entity IDs – after a re-pair there is no need to recreate or rename entities. After the new pairing, the Child lock switch started working.

That points to the problem being not in hardware, model or firmware, but in the device state / initialisation in ZHA or in how the cluster and quirk bind after the original interview. Test: the repaired unit was left untouched. If Child lock keeps working after a longer time and TRV sleep, the suspicion falls on a broken original interview. If it stops, the debug of the working and non-working states needs comparing along the path ZONNSMARTChildLock → ZONNSMARTManufCluster and the online_set / sleep state.

Side note

The generic switch names were translated through customize_glob so they apply to all identical TRVs: Boost (quick heat-up), Child lock, Online mode. The quirk itself does not need changing for the names.

IKEA SOMRIG button (19 October 2025)

The IKEA of Sweden SOMRIG shortcut button has two buttons, each on its own endpoint, and uses its own ShortcutV2Cluster (64640). Without a device handler, ZHA gets no usable automation triggers out of it.

The fix

A CustomDevice with a signature of two endpoints (profile 260, device type REMOTE_CONTROL). The replacement swaps in PowerConfig1AAACluster and ShortcutV2Cluster. The key is device_automation_triggers – each of the two buttons (endpoint 1 and 2) maps five actions:

device_automation_triggers = {
    (PRESSED, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: COMMAND_M_INITIAL_PRESS},
    (SHORT_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: COMMAND_M_SHORT_RELEASE},
    (DOUBLE_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: COMMAND_M_MULTI_PRESS_COMPLETE},
    (LONG_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: COMMAND_M_LONG_PRESS},
    (LONG_RELEASE, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: COMMAND_M_LONG_RELEASE},
    # same for BUTTON_2 on ENDPOINT_ID: 2
}

With the quirk in place, ZHA exposes press, short and long press, release and double press triggers for both buttons, which automations then hang off.

_Each section carries the date its case happened; the text was last updated in July 2026._

← back to explore