Tuya _TZE284_debczeci mmWave radar custom quirk

A battery-powered Tuya 24G mmWave presence radar (_TZE284_debczeci / TS0601) paired in ZHA but exposed no occupancy entity. The fix was a quirks-v2 TuyaQuirkBuilder custom quirk — but most of the debugging time went into things that had nothing to do with the DP mapping. Sharing the lessons because they generalize to any "unsupported Tuya device in ZHA" situation.

A working quirk for me (A rough one developed via trial and error)

Save to /config/custom_zha_quirks/ (filename ending .py), with zha: custom_quirks_path: /config/custom_zha_quirks/ in configuration.yaml:

python

"""ZHA quirk for _TZE284_debczeci TS0601 - 24G mmWave presence radar."""
from __future__ import annotations

import zigpy.types as t
from zigpy.quirks.v2.homeassistant import EntityType
from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass
from zigpy.zcl.clusters.measurement import OccupancySensing
from zigpy.zcl.clusters.general import PowerConfiguration

from zhaquirks.tuya import TuyaLocalCluster
from zhaquirks.tuya.builder import TuyaQuirkBuilder


class TuyaOccupancySensing(OccupancySensing, TuyaLocalCluster):
    """Occupancy cluster backed by Tuya DP1."""


class TuyaPowerConfiguration(PowerConfiguration, TuyaLocalCluster):
    """Power config cluster backed by Tuya DP4."""


class RadarSensitivity(t.enum8):
    """Radar sensitivity (DP9)."""
    Low = 0x00
    Medium = 0x01
    High = 0x02


class FadingTime(t.enum8):
    """Presence clear delay (DP10)."""
    S15 = 0x00
    S30 = 0x01
    S60 = 0x02


def _presence(value):
    # This unit uses trueFalse0: 0 = present, 1 = absent.
    # ZCL occupancy: 1 = occupied, 0 = unoccupied.
    return 1 if int(value) == 0 else 0


def _battery(value):
    # DP4: 0-100 percent -> ZCL battery_percentage_remaining is 0-200 (half-percent).
    return min(200, int(value) * 2)


(
    TuyaQuirkBuilder("_TZE284_debczeci", "TS0601")
    .applies_to("_TZE204_debczeci", "TS0601")
    .adds(TuyaOccupancySensing)
    .adds(TuyaPowerConfiguration)
    .tuya_dp(
        dp_id=1,
        ep_attribute=TuyaOccupancySensing.ep_attribute,
        attribute_name=OccupancySensing.AttributeDefs.occupancy.name,
        converter=_presence,
    )
    .tuya_dp(
        dp_id=4,
        ep_attribute=TuyaPowerConfiguration.ep_attribute,
        attribute_name=PowerConfiguration.AttributeDefs.battery_percentage_remaining.name,
        converter=_battery,
    )
    .binary_sensor(
        OccupancySensing.AttributeDefs.occupancy.name,
        TuyaOccupancySensing.cluster_id,
        device_class=BinarySensorDeviceClass.OCCUPANCY,
        entity_type=EntityType.STANDARD,
        translation_key="occupancy",
        fallback_name="Occupancy",
    )
    .tuya_enum(
        dp_id=9,
        attribute_name="radar_sensitivity",
        enum_class=RadarSensitivity,
        translation_key="radar_sensitivity",
        fallback_name="Radar sensitivity",
    )
    .tuya_enum(
        dp_id=10,
        attribute_name="fading_time",
        enum_class=FadingTime,
        translation_key="fading_time",
        fallback_name="Fading time",
    )
    .add_to_registry()
)

(DP map for this unit: DP1 presence, DP4 battery. Confirm yours by capturing live frames.)