My Tuya device doesn't work with ZHA - Or how to build a Tuya Quirk

So, you find yourself here because you purchased a Tuya device, it may have been sold under another brand name, but if the manufacturer name looks like _TZE284_7ytb3h8u, it’s still Tuya. You proceeded to pair your new device to ZHA only to find that it has no entities, the entities it has aren’t functioning, or it’s missing entities that it should have. Congratulations, you have found yourself in need of a quirk.

Before we continue, this guide only covers building quirks using TuyaQuirkBuilder based on the new V2 quirks. V1 quirks are not covered here and are in the process of being depreciated. To help you identify a V2 vs a V1 quirk examples are listed below, both for the same device.

V1 Quirk Example
quirk_id = TUYA_PLUG_MANUFACTURER

signature = {
    MODELS_INFO: [
        ("_TZE200_laqjm8qd", "TS0601"),
    ],
    ENDPOINTS: {
        # <SimpleDescriptor endpoint=1 profile=260 device_type=81
        # input_clusters=[0x0000,0x0004,0x0005,0x0006,0xEF00]
        # output_clusters=[0x000A,0x0019]>
        1: {
            PROFILE_ID: zha.PROFILE_ID,
            DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
            INPUT_CLUSTERS: [
                Basic.cluster_id,
                Groups.cluster_id,
                Scenes.cluster_id,
                TuyaOnOffManufCluster.cluster_id,
            ],
            OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
        },
        # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
        # input_clusters=[]
        # output_clusters=[33]
        242: {
            PROFILE_ID: zgp.PROFILE_ID,
            DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
            INPUT_CLUSTERS: [],
            OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
        },
    },
}

replacement = {
    ENDPOINTS: {
        1: {
            DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
            INPUT_CLUSTERS: [
                Basic.cluster_id,
                Groups.cluster_id,
                Scenes.cluster_id,
                TuyaOnOffManufCluster,
                TuyaOnOffNM,
            ],
            OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
        },
        2: {
            PROFILE_ID: zha.PROFILE_ID,
            DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
            INPUT_CLUSTERS: [
                TuyaOnOffNM,
            ],
            OUTPUT_CLUSTERS: [],
        },
        3: {
            PROFILE_ID: zha.PROFILE_ID,
            DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
            INPUT_CLUSTERS: [
                TuyaOnOffNM,
            ],
            OUTPUT_CLUSTERS: [],
        },
        4: {
            PROFILE_ID: zha.PROFILE_ID,
            DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
            INPUT_CLUSTERS: [
                TuyaOnOffNM,
            ],
            OUTPUT_CLUSTERS: [],
        },
        5: {
            PROFILE_ID: zha.PROFILE_ID,
            DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
            INPUT_CLUSTERS: [
                TuyaOnOffNM,
            ],
            OUTPUT_CLUSTERS: [],
        },
        242: {
            PROFILE_ID: zgp.PROFILE_ID,
            DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
            INPUT_CLUSTERS: [],
            OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
        },
    }
}
V2 (TuyaQuirkBuilder) Quirk Example
from zigpy.quirks.v2 import EntityType
import zigpy.types as t

from zhaquirks.tuya.builder import TuyaQuirkBuilder


(
    TuyaQuirkBuilder("_TZE200_laqjm8qd", "TS0601")
    .tuya_switch(
        dp_id=1,
        attribute_name="on_off_1",
        entity_type=EntityType.STANDARD,
        translation_key="on_off_1",
        fallback_name="Switch 1",
    )
    .tuya_switch(
        dp_id=2,
        attribute_name="on_off_2",
        entity_type=EntityType.STANDARD,
        translation_key="on_off_2",
        fallback_name="Switch 2",
    )
    .tuya_switch(
        dp_id=3,
        attribute_name="on_off_3",
        entity_type=EntityType.STANDARD,
        translation_key="on_off_3",
        fallback_name="Switch 3",
    )
    .tuya_switch(
        dp_id=4,
        attribute_name="on_off_4",
        entity_type=EntityType.STANDARD,
        translation_key="on_off_4",
        fallback_name="Switch 4",
    )
    .tuya_switch(
        dp_id=5,
        attribute_name="on_off_5",
        entity_type=EntityType.STANDARD,
        translation_key="on_off_5",
        fallback_name="Switch 5",
    )
    .skip_configuration()
    .add_to_registry()
)

Tuya Warning:

Before proceeding to the next steps, you should know that Tuya devices are some of the worst Zigbee devices you can purchase, as of today, almost all of the device support request in zigpy/zha-device-handlers are for Tuya devices. If you have purchased one or are planning to, you should know the following.

  1. Tuya devices do not follow the Zigbee Cluster Library (ZCL), so any new device has to be supported manually with a quirk. This isn’t a defect in ZHA, it’s just that your specific device hasn’t had a quirk added to HA yet. If Tuya followed the ZCL, we wouldn’t need a quirk.
  2. Tuya Devices are widely known to be junk, you should expect any of the following.
    – Early device failure.
    – Failing to report values.
    – Reporting incorrect values.
    – Devices acting as routers but failing to route traffic.
    – Devices bought at the same time and reporting the same model and manufacturer having differing functionality.
    – Devices flooding the Zigbee network with traffic causing network failure.
    – Devices destructively failing and risking fire or electrical hazard.

In summary, don’t buy Tuya, now, that said, I do have a few Tuya devices on my network, and there are many others successfully using them. So, it can be done, and there are reasons to do it, but know the risks before you do.

Prerequisites:

  1. Ensure you have custom quirks enabled, see ZHA Documentation.
  2. Know how to enable ZHA debug logging.
  3. This is an iterative process, be prepared for that.

Adding a Tuya device:

As outlined above, Tuya doesn’t use the ZCL, so going to zigpy/zha-device-handlers and requesting support with the device signature is unlikely to result in a quirk. Tuya transmits everything as a Tuya Datapoint (DP) to a Tuya cluster (0xEF00) and these won’t be listed in the device signature. So, the first step in supporting a new device is to identify the Tuya Datapoints and the datatype for each one.

Finding Tuya Datapoints:

There are three main methods for finding the Tuya DPs, listed in order of difficulty.
  1. Find the Tuya DPs from the Tuya developer console. See the z2m guide.
  2. Find an existing Z2M converter.
  3. Find the DPs from the diagnostic output of Local Tuya.

You should end up with a list of DPs such as this example from a real device and pulled from Tuya developer console.

Example Tuya DPs
"1":"Switch"
"2":"Start"
"101":"Last irrigation time."
"102":"Next irrigation time."
"103":"Real-time irrigation method (frequency)"
"104":"Duration"
"105":"Real-time Irrigation Interval (Seconds)"
"106":"Current temperature (Celsius)"
"107":"Smart Weather"
"108":"Current battery level (%)"
"109":"Circular irrigation parameters."
"110":"Real-time cumulative duration (seconds)"
"111":"Real-time accumulated water volume (liters)"
"112":"Other extensions."
"113":"Timing function."
"114":"统计功能"
"115":"时区"

Now that we have a list of DPs, the next step is to check and see if a similar quirk already exists and just needs the signature updated. Taking the list of DPs you gathered earlier, check the source code for ZHA device handlers for a similar quirk. In the example above we would look in tuya/ts0601_valve.py and see that there is a matching quirk already, we just need to add an additional applies_to to cover this device.

Building a Quirk:

This should be an iterative process, it's best to add a DP, test the quirk, then add the next DP. Adding them all at once is likely to make troubleshooting much harder.

Assuming that we were unable to find a similar quirk, we would then need to create one. At this point we need to identify the DPs we care about and then identify the correct datatypes. Continuing the irrigation controller example above, we would pick DPs 1,2,101,102,104,105,107,108,111,114.

Now that we have the DPs, we need identify the datatype for each one. What entity should represent it and if the device reports it normally or if we need to do a conversion.

First, let’s build out a barebones V2 quirk.

from zigpy.quirks.v2 import EntityType
import zigpy.types as t

from zhaquirks.tuya.builder import TuyaQuirkBuilder

(
    TuyaQuirkBuilder("_TZE200_laqjm8qd", "TS0601")
    .skip_configuration()
    .add_to_registry()
)

Now let’s add a battery sensor for DP 108

.tuya_battery(dp_id=108, power_cfg=TuyaPowerConfigurationCluster4AA)

DP 1 is a switch that controls the valve mode, we could use a regular switch via .tuya_switch but let’s set this up as an enum. Here we add an enum, then we add the dp to attribute converter using tuya_dp_attribute then finally add the enum entity with the appropriate attribute_name and enum_class.

class IrrigationMode(t.enum8):
    """Irrigation Mode Enum."""

    Duration = 0x00
    Capacity = 0x01

   .tuya_dp_attribute(
        dp_id=1,
        attribute_name="irrigation_mode",
        type=t.Bool,
    )
    .enum(
        attribute_name="irrigation_mode",
        cluster_id=TUYA_CLUSTER_ID,
        enum_class=IrrigationMode,
        translation_key="irrigation_mode",
        fallback_name="Irrigation mode",
    )

Next, let’s grab the valve or DP 2, since there is only one on off switch, we can simply just add.

.tuya_onoff(dp_id=2)

If we had more than one switch, we would need to add a tuya_switch for each one.

This valve has metering, so that’s simply.

.tuya_metering(dp_id=111)

Note, this works since the default metering class is water metering, if we were metering something else, we would need to provide our own metering_cfg. See TuyaValveWaterConsumed for the default metering class.

DP 103 is a used to set the number of irrigation cycles, a number entity is appropriate. Min and max values may not be shown in the Tuya dev console, so we can just pick sane ones.

    .tuya_number(
        dp_id=103,
        attribute_name="irrigation_cycles",
        type=t.uint8_t,
        min_value=0,
        max_value=100,
        step=1,
        translation_key="irrigation_cycles",
        fallback_name="Irrigation cycles",
    )

DP 114 represent the duration of the last irrigation in seconds, in typical Tuya fashion, this isn’t reported as integer and instead is reported as a string, so we need to convert it.

def tuya_string_to_td(v: str) -> int:
    """Convert String Duration to seconds."""
    dt = datetime.strptime(v, "%H:%M:%S,%f")
    return timedelta(hours=dt.hour, minutes=dt.minute, seconds=dt.second).seconds

Then with the conversion function, the sensor becomes. If this was a simpler conversion, a lambda would be acceptable.

    .tuya_sensor(
        dp_id=114,
        attribute_name="irrigation_duration",
        type=t.uint32_t,
        converter=lambda x: tuya_string_to_td(x),
        state_class=SensorStateClass.MEASUREMENT,
        device_class=SensorDeviceClass.DURATION,
        unit=UnitOfTime.SECONDS,
        translation_key="irrigation_duration",
        fallback_name="Last irrigation duration",
    )

The rest of the Datapoints are similar to the above and are omitted.

For further TuyaQuirkBuilder documentation see the draft documentation.

Next Steps

If you have a completed quirk that works, please submit a PR to zigpy/zha-device-handlers. There are way too many custom quirks floating around that were never submitted, help your fellow users out and submit yours.

If you need help or have issues, create an issue in the quirk repo or post here, remember, we can’t help you if you don’t have the Datapoints. We don’t have the device and can’t get the DPs, nor can we test, so you need to be willing to do the work to find them, create a quirk and test changes.

Some Tuya devices are harder to work with, some devices require sniffing traffic via Wireshark and reverse engineering to function. Not super common, but it’s possible that your device may not be supportable.

Good Luck!

3 Likes

This looks very useful. I have a Tuya based TRV for which there isn’t a quirk that works fully for it but there is one that does partially work. I want to see if I can modify it although it 's daunting. Your guide here has inspired me! However, the existing partial quirk is V1. Is there a V1 to V2 converter or guide for how to convert a quirk?

There isn’t a converter, and there isn’t a guide that I know of. Post a link to the v1 quirk you are working with, and I should be able to point you in the right direction.

This is the file ts0601_trv.py. It’s a bit of a monster! Thanks.

In that file, they are defined as such.

SITERWELL_CHILD_LOCK_ATTR = 0x0107  # [0] unlocked [1] child-locked
SITERWELL_WINDOW_DETECT_ATTR = 0x0112  # [0] inactive [1] active
SITERWELL_VALVE_DETECT_ATTR = 0x0114  # [0] do not report [1] report
SITERWELL_VALVE_STATE_ATTR = 0x026D  # [0,0,0,55] opening percentage
SITERWELL_TARGET_TEMP_ATTR = 0x0202  # [0,0,0,210] target room temp (decidegree)
SITERWELL_TEMPERATURE_ATTR = 0x0203  # [0,0,0,200] current room temp (decidegree)
SITERWELL_BATTERY_ATTR = 0x0215  # [0,0,0,98] battery charge
SITERWELL_MODE_ATTR = 0x0404  # [0] off [1] scheduled [2] manual

So, the DPs are actually the last byte. so 0x0107 is DP 7, 0x026D is 109 and so on.

1 Like

Thanks, I’ll start looking at a dedicated V2 quirk. Btw, I’d also come across this which has a few DP mappings that align with this quirk. Unfortunately I can’t get the device’s Tuya DPs using your method 1 as I use a RaspBee II to connect to my ZigBee devices.

But how would method 3 using local Tuya work with a ZigBee device?

I haven’t used it and really don’t know anything about it. I just helped with a quirk and the user had retrieved the diagnostics from local Tuya and it was a nice listing of DPs and data types, so added it here as a possibility if you did have it configured.

1 Like

I’d started looking at producing a V2 version of the quirk when I came across another quirk that claims to support the valve so I’ve paused looking at an update. For reference it’s this quirk.