This post documents my journey debugging and fixing a Moes ZHT-002 / TS0601 thermostat (manufacturer ID _TZE204_xalsoe3m) when connected to Home Assistant using ZHA.
Out of the box, ZHA recognized it as a TS0601 Tuya device, but exposed almost no useful controls — just a generic “Router” entity and a few disabled diagnostics.
It became clear that I needed a custom ZHA quirk.
ZHA loaded the device using the generic Tuya TS0601 handler, but no climate controls worked:
zigpy.device.DeviceHome Assistant logs were silent about quirk application:
Device: _TZE204_xalsoe3m TS0601
Quirk: zigpy.device.Device
It was time to make my own.
Using ZHA’s device info dump:
{
"manufacturer": "_TZE204_xalsoe3m",
"model": "TS0601",
"endpoints": {
"1": {
"profile_id": "0x0104",
"device_type": "0x0051",
"input_clusters": ["0x0000","0x0004","0x0005","0xef00"],
"output_clusters": ["0x000a","0x0019"]
}
}
}
This confirmed it was a Tuya MCU device using the EF00 cluster.
Custom quirks live under:
/config/custom_zha_quirks/tuya/
I started with a minimal working version of tuya_thermostat.py that matched the fingerprint and confirmed it loaded:
Loaded custom quirks. Please contribute them to https://github.com/zigpy/zha-device-handlers
That first milestone meant: ✅ my code was being loaded.
Tuya thermostats expose data via datapoints (DPs) rather than standard attributes.
To debug, I instrumented the MCU cluster:
def _dp_2_attr_update(self, dp_value):
import logging
logger = logging.getLogger("tuya.tuya_thermostat")
dp_id = getattr(dp_value, "dp", None)
tuya_data = getattr(dp_value, "data", None)
raw_bytes = getattr(tuya_data, "data", None)
decoded = getattr(tuya_data, "as_value", raw_bytes)
logger.warning(f"[DP→Attr] {self.endpoint.device.manufacturer} dp={dp_id} decoded={decoded}")
This produced live logs of all Tuya datapoints:
[DP→Attr] _TZE204_xalsoe3m dp=18 decoded=2100
[DP→Attr] _TZE204_xalsoe3m dp=50 decoded=2000
From this, I discovered:
| DP | Meaning | Notes |
|---|---|---|
| 16 | Actual temperature (°C × 10) | |
| 18 | Room temperature sensor value | |
| 40 | Child lock (bool) | |
| 50 | Setpoint temperature (°C × 100 scaling confirmed) |
That meant the scaling was 10× off in my first attempt!
After trial and error, and comparing with Zigbee2MQTT’s Moes ZHT-002 mapping, I confirmed:
.tuya_dp(
dp_id=16,
ep_attribute=TuyaThermostat.ep_attribute,
attribute_name=TuyaThermostat.AttributeDefs.local_temperature.name,
converter=lambda x: x * 10,
)
.tuya_dp(
dp_id=50,
ep_attribute=TuyaThermostat.ep_attribute,
attribute_name=TuyaThermostat.AttributeDefs.occupied_heating_setpoint.name,
converter=lambda x: x // 10,
dp_converter=lambda x: x * 10,
)
This finally produced working temperature reporting and adjustable setpoints within Home Assistant.
Borrowing ideas from ts0601_trv.py (the ZHA TRV quirk) and Zigbee2MQTT’s Tuya converter, I added:
These are declared using .tuya_switch(), .tuya_enum(), and .tuya_number() calls in the builder.
Here’s the simplified working core for _TZE204_xalsoe3m:
(
TuyaQuirkBuilder("_TZE204_xalsoe3m", "TS0601")
.tuya_dp(16,
ep_attribute=TuyaThermostat.ep_attribute,
attribute_name=TuyaThermostat.AttributeDefs.local_temperature.name,
converter=lambda x: x * 10,
)
.tuya_dp(50,
ep_attribute=TuyaThermostat.ep_attribute,
attribute_name=TuyaThermostat.AttributeDefs.occupied_heating_setpoint.name,
converter=lambda x: x // 10,
dp_converter=lambda x: x * 10,
)
.tuya_switch(
dp_id=40,
attribute_name="child_lock",
translation_key="child_lock",
fallback_name="Child lock",
)
.tuya_enum(
dp_id=23,
attribute_name="working_day",
enum_class=WorkingDayV02,
translation_key="working_day",
fallback_name="Working day mode",
)
.tuya_number(
dp_id=19,
attribute_name=TuyaThermostat.AttributeDefs.local_temperature_calibration.name,
type=t.int16s,
min_value=-9,
max_value=9,
step=1,
unit=UnitOfTemperature.CELSIUS,
translation_key="local_temperature_calibration",
fallback_name="Temperature calibration",
)
.adds(TuyaThermostat)
.skip_configuration()
.add_to_registry()
)
After restarting Home Assistant, ZHA loaded:
Quirk: zhaquirks.tuya.tuya_thermostat
The climate entity now exposes:
heat / off)And yes — [DP→Attr] logs still print decoded DP updates for debugging!
.as_value is your friend.The TS0601 thermostats are not hopeless — they just need a translator.
With a working custom quirk, ZHA can handle them as cleanly as Zigbee2MQTT, no extra gateway needed.
If you’re wrestling with a Tuya TS0601 device, start by identifying your DP map — everything else falls into place from there.
Repository: leocrawford.github.io
Device: _TZE204_xalsoe3m (Moes ZHT-002)
Home Assistant Core: 2025.10.4
Integration: ZHA (Texas Instruments CC2531 coordinator)