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, attribute0x0002correctly, but it does not send reliable IAS reports on its own. Solved with internal polling in the quirk. - Tuya EF00:
query_data,setDataandsendDatafailed 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_sourceand0xfffefrom the Basic cluster and only then continues configuring. The same was added to the Basic cluster. - Basic init: writing
0xffde = 13andmanufacturer 0x1141is safe and was added. - sendData – the breakthrough: after Basic init and the Z2M magic read,
TUYA_SEND_DATAstarted working with no further hacks. The write was verified onDP114(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
TuyaQuirkBuilderV2: the original V1 approach hit newer ZHA. Newer zigpy requirestranslation_key, without which the builder refuses to create entities. -
device_class()was used wrongly and raised anAssertionErrorinCustomDeviceV2. 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 meven though the device had sent nothing. Removed. New rule: if the device sent nothing, the value isunknownor comes from the HA cache. -
query_datareturnedUNSUP_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
0xEF00and manufacturer0x1002(4098 dec). The quirk addresses functions by a compositecommand_id(the high byte is the data type, the low byte the Tuya DP): Child lock296(0x0128, DP 40), Boost613(0x0265, DP 101), Online371(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.
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._