Nedis SmartLife ZBHTR20WT "not supported"

Hi there,

is there a way to get that thermostat to Supported state?
I found this on github:

Is there a way to get this into z2m - for noobs?

See Support new devices | Zigbee2MQTT and use the definition from github

1 Like

Thanks, but I don’t get it to work.
When I add that part to the z2m configuration.yaml, z2m doesn’t start up anymore (I edited everything to fit my .js file)
advanced:
log_level: debug
external_converters:
- WSDCGQ01LM.js

Can you elaborate a bit further?

Forget it - I had the stupid

I don’t get any debug messages in the log.
Do you have an idea why that could be?
In advanced settings on z2m I checked log level debug
log output is console first
under external converters my .js file shows up
Restarted z2mqtt

Device is still described as “unsupported” and debug log is not showing any message

only messages I get are those:
info 2024-11-18 14:29:24z2m:mqtt: MQTT publish: topic 'zigbee2mqtt/0xa4c1385c0166c8f1', payload '{"last_seen":"2024-11-18T13:29:25.265Z","linkquality":81}'

Well, are you sure the fingerprint matches yours?

What does yours say?

1 Like

Thanks for the reply, I dug a bit further and found this in the logs:
[2024-11-18 14:26:36] error: z2m: Failed to load external converter file ‘ZBHTR20WT.js’ (Cannot find module ‘zigbee-herdsman-converters/lib/extend’

I am currently running Z2MQTT 1.41.0-1
I’ve read something about z2m Edge version, but these posts are from 2022.

The generated file says this:

Just remove the line

Hah, that did the trick.

Thank you so much!

Has anyone turned that into a ZHA configuration/quirk? Otherwise I’ll look into it…

1 Like

Sweet bananas! … and I immediately found a bug. :grin:

Can’t set the thermostat to a different temperature via UI. If you try, the set temperature jumps to 160C and the UI complains that it’s not a valid one.

That would be little hot. I don’t have issues, but dont use latest quirk yet, see below code in my quirk

"""Map from manufacturer to standard clusters for thermostatic valves."""

from zigpy.profiles import zha
from zigpy.quirks.v2.homeassistant import PERCENTAGE, UnitOfTemperature
from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass
from zigpy.quirks.v2.homeassistant.sensor import SensorStateClass
import zigpy.types as t
from zigpy.zcl import foundation
from zigpy.zcl.clusters.hvac import RunningState, Thermostat

from zhaquirks.tuya.builder import TuyaQuirkBuilder
from zhaquirks.tuya.mcu import TuyaAttributesCluster


class TuyaThermostatSystemMode(t.enum8):
    """Tuya thermostat system mode enum."""

    Auto = 0x00
    Heat = 0x01
    Off = 0x02


class TuyaThermostatEcoMode(t.enum8):
    """Tuya thermostat eco mode enum."""

    Comfort = 0x00
    Eco = 0x01


class State(t.enum8):
    """State option."""

    Off = 0x00
    On = 0x01


class BatteryState(t.enum8):
    """Battery state option."""

    Normal = 0x00
    Low = 0x01


class ScheduleState(t.enum8):
    """Schedule state option."""

    Disabled = 0x00
    Enabled = 0x01


class TuyaThermostatV2(Thermostat, TuyaAttributesCluster):
    """Tuya local thermostat cluster."""

    manufacturer_id_override: t.uint16_t = foundation.ZCLHeader.NO_MANUFACTURER_ID
    _CONSTANT_ATTRIBUTES = {
        Thermostat.AttributeDefs.min_heat_setpoint_limit.id: 500,
        Thermostat.AttributeDefs.max_heat_setpoint_limit.id: 3000,
        Thermostat.AttributeDefs.ctrl_sequence_of_oper.id: Thermostat.ControlSequenceOfOperation.Heating_Only,
    }

    def __init__(self, *args, **kwargs):
        """Init a TuyaThermostat cluster."""
        super().__init__(*args, **kwargs)
        self.add_unsupported_attribute(
            Thermostat.AttributeDefs.setpoint_change_source.id
        )
        self.add_unsupported_attribute(
            Thermostat.AttributeDefs.setpoint_change_source_timestamp.id
        )
        self.add_unsupported_attribute(Thermostat.AttributeDefs.pi_heating_demand.id)

    async def write_attributes(self, attributes, manufacturer=None):
        """Overwrite to force manufacturer code."""
        return await super().write_attributes(
            attributes, manufacturer=foundation.ZCLHeader.NO_MANUFACTURER_ID
        )


(
    TuyaQuirkBuilder("_TYST11_KGbxAXL2", "GbxAXL2")
    .applies_to("_TYST11_c88teujp", "88teujp")
    .applies_to("_TYST11_azqp6ssj", "zqp6ssj")
    .applies_to("_TYST11_yw7cahqs", "w7cahqs")
    .applies_to("_TYST11_9gvruqf5", "gvruqf5")
    .applies_to("_TYST11_zuhszj9s", "uhszj9s")
    .applies_to("_TYST11_caj4jz0i", "aj4jz0i")
    .applies_to("_TZE200_c88teujp", "TS0601")
    .applies_to("_TZE200_azqp6ssj", "TS0601")
    .applies_to("_TZE200_yw7cahqs", "TS0601")
    .applies_to("_TZE200_9gvruqf5", "TS0601")
    .applies_to("_TZE200_zuhszj9s", "TS0601")
    .applies_to("_TZE200_zr9c0day", "TS0601")
    .applies_to("_TZE200_0dvm9mva", "TS0601")
    .applies_to("_TZE200_h4cgnbzg", "TS0601")
    .applies_to("_TZE200_exfrnlow", "TS0601")
    .applies_to("_TZE200_9m4kmbfu", "TS0601")
    .applies_to("_TZE200_3yp57tby", "TS0601")
    # default device type is `SMART_PLUG` for this,
    # so change it back to keep UID/entity the same
    .replaces_endpoint(1, device_type=zha.DeviceType.THERMOSTAT)
    .tuya_dp(
        dp_id=3,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.running_state.name,
        converter=lambda x: RunningState.Heat_State_On if x else RunningState.Idle,
    )
    .tuya_switch(
        dp_id=8,
        attribute_name="window_detection",
        translation_key="window_detection",
        fallback_name="Open window detection",
    )
    .tuya_switch(
        dp_id=10,
        attribute_name="frost_protection",
        translation_key="frost_protection",
        fallback_name="Frost protection",
    )
    .tuya_number(
        dp_id=27,
        attribute_name=TuyaThermostatV2.AttributeDefs.local_temperature_calibration.name,
        type=t.uint32_t,
        min_value=-6,
        max_value=6,
        unit=UnitOfTemperature.CELSIUS,
        step=1,
        translation_key="local_temperature_calibration",
        fallback_name="Local temperature calibration",
    )
    .tuya_switch(
        dp_id=40,
        attribute_name="child_lock",
        translation_key="child_lock",
        fallback_name="Child lock",
    )
    .tuya_dp(
        dp_id=101,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.system_mode.name,
        converter=lambda x: {
            True: Thermostat.SystemMode.Heat,
            False: Thermostat.SystemMode.Off,
        }[x],
        dp_converter=lambda x: {
            Thermostat.SystemMode.Heat: True,
            Thermostat.SystemMode.Off: False,
        }[x],
    )
    .tuya_dp(
        dp_id=102,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.local_temperature.name,
        converter=lambda x: x * 10,
    )
    .tuya_dp(
        dp_id=103,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.occupied_heating_setpoint.name,
        converter=lambda x: x * 10,
        dp_converter=lambda x: x // 10,
    )
    .adds(TuyaThermostatV2)
    .tuya_sensor(
        dp_id=104,
        attribute_name="valve_position",
        type=t.int16s,
        divisor=10,
        state_class=SensorStateClass.MEASUREMENT,
        unit=PERCENTAGE,
        translation_key="valve_position",
        fallback_name="Valve position",
    )
    .tuya_binary_sensor(
        dp_id=105,
        attribute_name="battery_low",
        device_class=BinarySensorDeviceClass.BATTERY,
        fallback_name="Battery low",
    )
    .tuya_switch(
        dp_id=106,
        attribute_name="away_mode",
        translation_key="away_mode",
        fallback_name="Away mode",
    )
    .tuya_switch(
        dp_id=108,
        attribute_name="schedule_enable",
        translation_key="schedule_enable",
        fallback_name="Schedule enable",
    )
    .tuya_switch(
        dp_id=130,
        attribute_name="scale_protection",
        translation_key="scale_protection",
        fallback_name="Scale protection",
    )
    .skip_configuration()
    .add_to_registry()
)


(
    TuyaQuirkBuilder("_TZE204_rtrmfadk", "TS0601")
    .tuya_dp(
        dp_id=1,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.system_mode.name,
        converter=lambda x: {
            TuyaThermostatSystemMode.Auto: Thermostat.SystemMode.Auto,
            TuyaThermostatSystemMode.Heat: Thermostat.SystemMode.Heat,
            TuyaThermostatSystemMode.Off: Thermostat.SystemMode.Off,
        }[x],
        dp_converter=lambda x: {
            Thermostat.SystemMode.Auto: TuyaThermostatSystemMode.Auto,
            Thermostat.SystemMode.Heat: TuyaThermostatSystemMode.Heat,
            Thermostat.SystemMode.Off: TuyaThermostatSystemMode.Off,
        }[x],
    )
    .tuya_dp(
        dp_id=2,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.occupied_heating_setpoint.name,
        converter=lambda x: x * 10,
        dp_converter=lambda x: x // 10,
    )
    .tuya_dp(
        dp_id=3,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.local_temperature.name,
        converter=lambda x: x * 10,
    )
    .tuya_dp(
        dp_id=6,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.running_state.name,
        converter=lambda x: RunningState.Heat_State_On if x else RunningState.Idle,
    )
    .tuya_binary_sensor(
        dp_id=7,
        attribute_name="window_open",
        device_class=BinarySensorDeviceClass.WINDOW,
        fallback_name="Window open",
    )
    .tuya_switch(
        dp_id=8,
        attribute_name="window_detection",
        translation_key="window_detection",
        fallback_name="Open window detection",
    )
    .tuya_switch(
        dp_id=12,
        attribute_name="child_lock",
        translation_key="child_lock",
        fallback_name="Child lock",
    )
    .tuya_battery(dp_id=13)
    .tuya_binary_sensor(
        dp_id=14,
        attribute_name="error_or_battery_low",
        device_class=BinarySensorDeviceClass.PROBLEM,
        translation_key="error_or_battery_low",
        fallback_name="Error or battery low",
    )
    .tuya_number(
        dp_id=15,
        attribute_name="min_temperature",
        type=t.uint16_t,
        min_value=1,
        max_value=15,
        unit=UnitOfTemperature.CELSIUS,
        step=1,
        translation_key="min_temperature",
        fallback_name="Min temperature",
    )
    .tuya_number(
        dp_id=16,
        attribute_name="max_temperature",
        type=t.uint16_t,
        min_value=15,
        max_value=35,
        unit=UnitOfTemperature.CELSIUS,
        step=1,
        translation_key="max_temperature",
        fallback_name="Max temperature",
    )
    .tuya_number(
        dp_id=101,
        attribute_name=TuyaThermostatV2.AttributeDefs.local_temperature_calibration.name,
        type=t.uint32_t,
        min_value=-6,
        max_value=6,
        unit=UnitOfTemperature.CELSIUS,
        step=1,
        translation_key="local_temperature_calibration",
        fallback_name="Local temperature calibration",
    )
    .tuya_enum(
        dp_id=114,
        attribute_name="eco_mode",
        enum_class=TuyaThermostatEcoMode,
        translation_key="eco_mode",
        fallback_name="Eco mode",
    )
    .adds(TuyaThermostatV2)
    .skip_configuration()
    .add_to_registry()
)


(
    TuyaQuirkBuilder("_TZE200_bvu2wnxz", "TS0601")
    .applies_to("_TZE200_6rdj8dzm", "TS0601")
    .applies_to("_TZE200_9xfjixap", "TS0601")
    .applies_to("_TZE200_p3dbf6qs", "TS0601")
    .applies_to("_TZE200_rxntag7i", "TS0601")
    .applies_to("_TZE200_yqgbrdyo", "TS0601")
    .applies_to("_TZE284_p3dbf6qs", "TS0601")
    .applies_to("_TZE200_rxq4iti9", "TS0601")
    .applies_to("_TZE200_hvaxb2tc", "TS0601")
    .applies_to("_TZE284_o3x45p96", "TS0601")
    .applies_to("_TZE284_c6wv4xyo", "TS0601")
    .applies_to("_TZE204_o3x45p96", "TS0601")
    .applies_to("_TZE204_ogx8u5z6", "TS0601")
    .applies_to("_TZE284_ogx8u5z6", "TS0601")
    .tuya_dp(
        dp_id=2,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.system_mode.name,
        converter=lambda x: {
            TuyaThermostatSystemMode.Auto: Thermostat.SystemMode.Auto,
            TuyaThermostatSystemMode.Heat: Thermostat.SystemMode.Heat,
            TuyaThermostatSystemMode.Off: Thermostat.SystemMode.Off,
        }[x],
        dp_converter=lambda x: {
            Thermostat.SystemMode.Auto: TuyaThermostatSystemMode.Auto,
            Thermostat.SystemMode.Heat: TuyaThermostatSystemMode.Heat,
            Thermostat.SystemMode.Off: TuyaThermostatSystemMode.Off,
        }[x],
    )
    .tuya_dp(
        dp_id=3,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.running_state.name,
        converter=lambda x: RunningState.Heat_State_On if not x else RunningState.Idle,
    )
    .tuya_dp(
        dp_id=4,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.occupied_heating_setpoint.name,
        converter=lambda x: x * 10,
        dp_converter=lambda x: x // 10,
    )
    .tuya_dp(
        dp_id=5,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.local_temperature.name,
        converter=lambda x: x * 10,
    )
    .tuya_number(
        dp_id=47,
        attribute_name=TuyaThermostatV2.AttributeDefs.local_temperature_calibration.name,
        type=t.uint32_t,
        min_value=-6,
        max_value=6,
        unit=UnitOfTemperature.CELSIUS,
        step=1,
        translation_key="local_temperature_calibration",
        fallback_name="Local temperature calibration",
    )
    .tuya_switch(
        dp_id=7,
        attribute_name="child_lock",
        translation_key="child_lock",
        fallback_name="Child lock",
    )
    .tuya_binary_sensor(
        dp_id=35,
        attribute_name="error_or_battery_low",
        device_class=BinarySensorDeviceClass.PROBLEM,
        translation_key="error_or_battery_low",
        fallback_name="Error or battery low",
    )
    .tuya_switch(
        dp_id=36,
        attribute_name="frost_protection",
        translation_key="frost_protection",
        fallback_name="Frost protection",
    )
    .tuya_switch(
        dp_id=39,
        attribute_name="scale_protection",
        translation_key="scale_protection",
        fallback_name="Scale protection",
    )
    .adds(TuyaThermostatV2)
    .skip_configuration()
    .add_to_registry()
)


(
    TuyaQuirkBuilder("_TZE200_ne4pikwm", "TS0601")  # Nedis ZBHTR20WT
    .applies_to("_TZE284_ne4pikwm", "TS0601")
    .tuya_dp(
        dp_id=3,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.running_state.name,
        converter=lambda x: RunningState.Heat_State_On if x else RunningState.Idle,
    )
    .tuya_switch(
        dp_id=8,
        attribute_name="window_detection",
        translation_key="window_detection",
        fallback_name="Open window detection",
    )
    .tuya_switch(
        dp_id=10,
        attribute_name="frost_protection",
        translation_key="frost_protection",
        fallback_name="Frost protection",
    )
    .tuya_number(
        dp_id=27,
        attribute_name=TuyaThermostatV2.AttributeDefs.local_temperature_calibration.name,
        type=t.uint32_t,
        min_value=-6,
        max_value=6,
        unit=UnitOfTemperature.CELSIUS,
        step=1,
        translation_key="local_temperature_calibration",
        fallback_name="Local temperature calibration",
    )
    .tuya_switch(
        dp_id=40,
        attribute_name="child_lock",
        translation_key="child_lock",
        fallback_name="Child lock",
    )
    .tuya_dp(
        dp_id=101,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.system_mode.name,
        converter=lambda x: {
            True: Thermostat.SystemMode.Heat,
            False: Thermostat.SystemMode.Off,
        }[x],
        dp_converter=lambda x: {
            Thermostat.SystemMode.Heat: True,
            Thermostat.SystemMode.Off: False,
        }[x],
    )
    .tuya_dp(
        dp_id=102,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.local_temperature.name,
        converter=lambda x: x * 10,
    )
    .tuya_dp(
        dp_id=103,
        ep_attribute=TuyaThermostatV2.ep_attribute,
        attribute_name=TuyaThermostatV2.AttributeDefs.occupied_heating_setpoint.name,
        converter=lambda x: x * 10,
        dp_converter=lambda x: x // 10,
    )
    .tuya_binary_sensor(
        dp_id=105,
        attribute_name="battery_low",
        device_class=BinarySensorDeviceClass.BATTERY,
        fallback_name="Battery low",
    )
    .tuya_switch(
        dp_id=106,
        attribute_name="leave_home",
        translation_key="leave_home",
        fallback_name="Leave home",
    )
    .tuya_switch(
        dp_id=108,
        attribute_name="schedule_mode",
        translation_key="schedule_mode",
        fallback_name="Schedule mode",
    )
    .tuya_switch(
        dp_id=130,
        attribute_name="scale_protection",
        translation_key="scale_protection",
        fallback_name="Scale protection",
    )
    .adds(TuyaThermostatV2)
    .skip_configuration()
    .add_to_registry()
)

I can compare versions later. I’ve deployed the official ZHA device handlers release 0.0.134.

You quirk helped to find the issue: It’s this:

-        Thermostat.AttributeDefs.abs_min_heat_setpoint_limit.id: 500,
-        Thermostat.AttributeDefs.abs_max_heat_setpoint_limit.id: 3000,
+        Thermostat.AttributeDefs.min_heat_setpoint_limit.id: 500,
+        Thermostat.AttributeDefs.max_heat_setpoint_limit.id: 3000,
1 Like

I’ve replied to the issue. The abs attributes should be used for the constant attributes. So, the upstream quirk is correct in that regard.

If you were using custom quirks before, you may need to completely remove and re-pair the device to clear attribute cache, as the non-abs values may be cached. They should not be set by the quirk.
At least currently, I don’t see an issue with the quirk, but we’ll investigate further in the issue. Please attach device diagnostics there.


In general, custom quirks are only meant to be temporarily used for testing.
If you want to have the new upstream quirks, you should try the Home Assistant Core 2025.3.0 betas, as they include the latest ZHA library (with the latest zigpy and zha-quirks 0.0.134). The full HA 2025.3.0 version will release in about two days, but the betas are already pretty stable at this point.

2 Likes

I have no problem waiting for the full release. (I usually don’t upgrade to .0 releases but in this case I’ll make an exception :wink:)

It was a rather weird timely coincidence that I bought a full set of those TRVs a week ago, hoping that they would already be supported but apparently were just being added so I had a time constraint to either getting them (close) to working or sending them back to not miss the return window and ending up with expensive paper weights…

1 Like

Nice, i had exactly the same timing for getting them work or sending it back :slight_smile:
glad i came quick to something working with all the efforts others already made