Repository: zigpy/zha-device-handlers
Pull request #3748 (2025-01-22 01:25:18)

Add Avatto thermostats (Tuya)
## Proposed change
<!--
  Explain your proposed change below.
-->

Adds Tuya ZWT198/ZWT100-BH Avatto wall thermostats

- _TZE204_lzriup1j
- _TZE200_viy9ihs7
- _TZE204_xnbkhhdr
- _TZE284_xnbkhhdr

Closes: #3627
Closes: #3212
Closes: #2589
Closes: #3482
Closes: #3326



## Additional information
<!--
  Please include any additional information that is important to this PR.
  For example, if this PR is a potentially breaking change, mention that here.
  If this PR requires other PRs to be merged in HA Core or other projects, mention that.
  Lastly, if this PR fixes a specific issue, please include "Fixes #xxxx".
-->

Based on: https://github.com/Koenkk/zigbee2mqtt/discussions/25752
and https://github.com/Koenkk/zigbee-herdsman-converters/blob/40e72bc2515deb67ef35dddab04437342b7b3eb9/src/devices/tuya.ts#L7027
## Checklist
<!--
  Put an 'x' in all boxes that apply.
  Note: You do not need to tick all boxes before creating a PR.
-->

- [x] The changes are tested and work correctly
- [x] `pre-commit` checks pass / the code has been formatted using Black
- [x] Tests have been added to verify that the new code works


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3748?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 90.12%. Comparing base [(`6c5fc22`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/6c5fc226ec94ff7a30c183888217cb5e0ed27be2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) to head [(`d4af9e9`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/d4af9e9e8fdc6a3f52e1681376952e4764e1b6ee?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##              dev    #3748      +/-   ##
==========================================
+ Coverage   90.09%   90.12%   +0.02%     
==========================================
  Files         320      320              
  Lines       10431    10459      +28     
==========================================
+ Hits         9398     9426      +28     
  Misses       1033     1033              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3748?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).

delaosa: I've tested this quirk with my_TZE284_xnbkhhdr, it works quite well however the time is not set on the device, it keeps trying to sync, due that there are a excessive number of messages". In Z2M this device works flawless (time is synced and no excessive number of messages).  This devices is configured with "respondToMcuVersionResponse: false", as I've also seen in another [issue](https://github.com/Koenkk/zigbee2mqtt/issues/24516) it makes a difference :  

> Important: respondToMcuVersionResponse should be false otherweise there are an avalanche of commandMcuVersionResponse messages every second.
prairiesnpr: @delaosa Thanks for the feedback! This should address both of your points.
delaosa: I cannot test it since .add_to_registry(replacement_cluster=NoManufTimeNoVersionRespTuyaMCUCluster) is not supported in my HA installation, guessing this feature hasn't be released yet. However I've tested updating TUYA_SET_TIME command in tuya/mcu/__init__.py with is_manufacturer_specific=False and time sync is working. Thanks!

Updating temperature or turning off/on the thermostat from HA is not working, to fix it I've added the following code to 'TuyaThermostat' class:

`
    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
        )
  ` 

Patch:
diff --git a/zhaquirks/tuya/ts0601_thermostat.py b/zhaquirks/tuya/ts0601_thermostat.py
deleted file mode 100644
index 36dc2f9..0000000
--- a/zhaquirks/tuya/ts0601_thermostat.py
+++ /dev/null
@@ -1,217 +0,0 @@
-"""Tuya TS0601 Thermostat."""
-
-from zigpy.quirks.v2.homeassistant import (
-    UnitOfElectricCurrent,
-    UnitOfElectricPotential,
-    UnitOfEnergy,
-    UnitOfPower,
-    UnitOfTemperature,
-)
-from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass
-from zigpy.types import t
-from zigpy.zcl import foundation
-from zigpy.zcl.clusters.hvac import Thermostat
-
-from zhaquirks.tuya.builder import TuyaQuirkBuilder
-from zhaquirks.tuya.mcu import TuyaAttributesCluster
-
-
-class RegulatorPeriod(t.enum8):
-    """Tuya Regulator Period enum."""
-
-    FifteenMin = 0x00
-    ThirtyMin = 0x01
-    FortyFiveMin = 0x02
-    SixtyMin = 0x03
-    NinetyMin = 0x04
-
-
-class ThermostatMode(t.enum8):
-    """Tuya Thermostat mode."""
-
-    Regulator = 0x00
-    Thermostat = 0x01
-
-
-class PresetMode(t.enum8):
-    """Tuya PresetMode enum."""
-
-    Manual = 0x00
-    Home = 0x01
-    Away = 0x02
-
-
-class SensorMode(t.enum8):
-    """Tuya SensorMode enum."""
-
-    Air = 0x00
-    Floor = 0x01
-    Both = 0x02
-
-
-class TuyaThermostat(Thermostat, TuyaAttributesCluster):
-    """Tuya local thermostat cluster."""
-
-    manufacturer_id_override: t.uint16_t = foundation.ZCLHeader.NO_MANUFACTURER_ID
-
-    _CONSTANT_ATTRIBUTES = {
-        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)
-
-
-(
-    TuyaQuirkBuilder("_TZE204_p3lqqy2r", "TS0601")
-    .tuya_dp(
-        dp_id=1,
-        ep_attribute=TuyaThermostat.ep_attribute,
-        attribute_name=TuyaThermostat.AttributeDefs.system_mode.name,
-        converter=lambda x: 0x00 if not x else 0x04,
-        dp_converter=lambda x: x != 0x00,
-    )
-    .tuya_enum(
-        dp_id=2,
-        attribute_name="preset_mode",
-        enum_class=PresetMode,
-        translation_key="preset_mode",
-        fallback_name="Preset mode",
-    )
-    .tuya_dp(
-        dp_id=16,
-        ep_attribute=TuyaThermostat.ep_attribute,
-        attribute_name=TuyaThermostat.AttributeDefs.occupied_heating_setpoint.name,
-        converter=lambda x: x * 100,
-        dp_converter=lambda x: x // 100,
-    )
-    .tuya_dp(
-        dp_id=24,
-        ep_attribute=TuyaThermostat.ep_attribute,
-        attribute_name=TuyaThermostat.AttributeDefs.local_temperature.name,
-        converter=lambda x: x * 100,
-    )
-    .tuya_dp(
-        dp_id=28,
-        ep_attribute=TuyaThermostat.ep_attribute,
-        attribute_name=Thermostat.AttributeDefs.local_temperature_calibration.name,
-        converter=lambda x: x * 100,
-        dp_converter=lambda x: x // 100,
-    )
-    .tuya_switch(
-        dp_id=30,
-        attribute_name="child_lock",
-        translation_key="child_lock",
-        fallback_name="Child lock",
-    )
-    .tuya_sensor(
-        dp_id=101,
-        attribute_name="local_temperature_floor",
-        type=t.int16s,
-        device_class=SensorDeviceClass.TEMPERATURE,
-        state_class=SensorStateClass.MEASUREMENT,
-        unit=UnitOfTemperature.CELSIUS,
-        translation_key="local_temperature_floor",
-        fallback_name="Floor temperature",
-    )
-    .tuya_enum(
-        dp_id=102,
-        attribute_name="temperature_sensor_select",
-        enum_class=SensorMode,
-        translation_key="sensor_mode",
-        fallback_name="Sensor mode",
-    )
-    .tuya_dp(
-        dp_id=104,
-        ep_attribute=TuyaThermostat.ep_attribute,
-        attribute_name=TuyaThermostat.AttributeDefs.running_state.name,
-        converter=lambda x: 0x00 if not x else 0x01,
-    )
-    .tuya_binary_sensor(
-        dp_id=106,
-        attribute_name="window_detection",
-        translation_key="window_detection",
-        fallback_name="Open window detection",
-    )
-    .tuya_dp(
-        dp_id=107,
-        ep_attribute=TuyaThermostat.ep_attribute,
-        attribute_name=TuyaThermostat.AttributeDefs.max_heat_setpoint_limit.name,
-        converter=lambda x: x * 100,
-        dp_converter=lambda x: x // 100,
-    )
-    .tuya_enum(
-        dp_id=108,
-        attribute_name="thermostat_mode",
-        enum_class=ThermostatMode,
-        translation_key="thermostat_mode",
-        fallback_name="Thermostat mode",
-    )
-    .tuya_enum(
-        dp_id=109,
-        attribute_name="regulator_period",
-        enum_class=RegulatorPeriod,
-        translation_key="regulator_period",
-        fallback_name="Regulator period",
-    )
-    .tuya_number(
-        dp_id=110,
-        attribute_name="regulator_set_point",
-        type=t.uint16_t,
-        unit=UnitOfTemperature.CELSIUS,
-        min_value=0,
-        max_value=100,
-        step=1,
-        translation_key="regulator_set_point",
-        fallback_name="Regulator set point",
-    )
-    .adds(TuyaThermostat)
-    .tuya_sensor(
-        dp_id=120,
-        attribute_name="current",
-        type=t.int16s,
-        divisor=10,
-        device_class=SensorDeviceClass.CURRENT,
-        state_class=SensorStateClass.MEASUREMENT,
-        unit=UnitOfElectricCurrent.AMPERE,
-        fallback_name="Current",
-    )
-    .tuya_sensor(
-        dp_id=121,
-        attribute_name="voltage",
-        type=t.int16s,
-        device_class=SensorDeviceClass.VOLTAGE,
-        state_class=SensorStateClass.MEASUREMENT,
-        unit=UnitOfElectricPotential.VOLT,
-        fallback_name="Voltage",
-    )
-    .tuya_sensor(
-        dp_id=122,
-        attribute_name="power",
-        type=t.int16s,
-        device_class=SensorDeviceClass.POWER,
-        state_class=SensorStateClass.MEASUREMENT,
-        unit=UnitOfPower.WATT,
-        fallback_name="Power",
-    )
-    .tuya_sensor(
-        dp_id=123,
-        attribute_name="energy",
-        type=t.int16s,
-        divisor=100,
-        device_class=SensorDeviceClass.ENERGY,
-        state_class=SensorStateClass.TOTAL,
-        unit=UnitOfEnergy.KILO_WATT_HOUR,
-        fallback_name="Energy",
-    )
-    .skip_configuration()
-    .add_to_registry()
-)
diff --git a/zhaquirks/tuya/ts0601_trv.py b/zhaquirks/tuya/ts0601_trv.py
index cf74868..e802635 100644
--- a/zhaquirks/tuya/ts0601_trv.py
+++ b/zhaquirks/tuya/ts0601_trv.py
@@ -1918,6 +1918,7 @@ class ZonnsmartTV01_ZG(TuyaThermostat):
     .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,
diff --git a/zhaquirks/tuya/tuya_sensor.py b/zhaquirks/tuya/tuya_sensor.py
index 767f29b..486f518 100644
--- a/zhaquirks/tuya/tuya_sensor.py
+++ b/zhaquirks/tuya/tuya_sensor.py
@@ -57,6 +57,7 @@ class TuyaNousTempHumiAlarm(t.enum8):
     .applies_to("_TZE200_qyflbnbj", "TS0601")
     .applies_to("_TZE284_qyflbnbj", "TS0601")
     .applies_to("_TZE200_44af8vyi", "TS0601")
+    .applies_to("_TZE200_vvmbj46n", "TS0601")
     # Not using tuya_temperature because device reports negative values incorrectly
     .tuya_dp(
         dp_id=1,
diff --git a/zhaquirks/tuya/tuya_thermostat.py b/zhaquirks/tuya/tuya_thermostat.py
new file mode 100644
index 0000000..5f6863b
--- /dev/null
+++ b/zhaquirks/tuya/tuya_thermostat.py
@@ -0,0 +1,451 @@
+"""Tuya TS0601 Thermostat."""
+
+import copy
+
+from zigpy.quirks.v2 import BinarySensorDeviceClass, EntityType
+from zigpy.quirks.v2.homeassistant import (
+    UnitOfElectricCurrent,
+    UnitOfElectricPotential,
+    UnitOfEnergy,
+    UnitOfPower,
+    UnitOfTemperature,
+)
+from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass
+from zigpy.types import t
+from zigpy.zcl import foundation
+from zigpy.zcl.clusters.hvac import Thermostat
+
+from zhaquirks.tuya import TUYA_SET_TIME, TuyaTimePayload
+from zhaquirks.tuya.builder import TuyaQuirkBuilder
+from zhaquirks.tuya.mcu import TuyaAttributesCluster, TuyaMCUCluster
+
+
+class RegulatorPeriod(t.enum8):
+    """Tuya regulator period enum."""
+
+    _15_min = 0x00
+    _30_min = 0x01
+    _45_min = 0x02
+    _60_min = 0x03
+    _90_min = 0x04
+
+
+class ThermostatMode(t.enum8):
+    """Tuya thermostat mode."""
+
+    Regulator = 0x00
+    Thermostat = 0x01
+
+
+class PresetModeV01(t.enum8):
+    """Tuya preset mode v01 enum."""
+
+    Manual = 0x00
+    Home = 0x01
+    Away = 0x02
+
+
+class PresetModeV02(t.enum8):
+    """Tuya preset mode v02 enum."""
+
+    Manual = 0x00
+    Auto = 0x01
+    Temporary_Manual = 0x02
+
+
+class PresetModeV03(t.enum8):
+    """Tuya preset mode v03 enum."""
+
+    Auto = 0x00
+    Manual = 0x01
+    Temporary_Manual = 0x02
+
+
+class SensorMode(t.enum8):
+    """Tuya sensor mode enum."""
+
+    Air = 0x00
+    Floor = 0x01
+    Both = 0x02
+
+
+class BacklightMode(t.enum8):
+    """Tuya backlight mode enum."""
+
+    Off = 0x00
+    Low = 0x01
+    Medium = 0x02
+    High = 0x03
+
+
+class WorkingDayV01(t.enum8):
+    """Tuya Working day v01 enum."""
+
+    Disabled = 0x00
+    Six_One = 0x01
+    Five_Two = 0x02
+    Seven = 0x03
+
+
+class WorkingDayV02(t.enum8):
+    """Tuya Working day v02 enum."""
+
+    Disabled = 0x00
+    Five_Two = 0x01
+    Six_One = 0x02
+    Seven = 0x03
+
+
+class TuyaThermostat(Thermostat, TuyaAttributesCluster):
+    """Tuya local thermostat cluster."""
+
+    manufacturer_id_override: t.uint16_t = foundation.ZCLHeader.NO_MANUFACTURER_ID
+
+    _CONSTANT_ATTRIBUTES = {
+        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)
+
+
+class NoManufTimeNoVersionRespTuyaMCUCluster(TuyaMCUCluster):
+    """Tuya Manufacturer Cluster with set_time mod."""
+
+    # Deepcopy required to override 'set_time', without, it will revert
+    server_commands = copy.deepcopy(TuyaMCUCluster.server_commands)
+    server_commands.update(
+        {
+            TUYA_SET_TIME: foundation.ZCLCommandDef(
+                "set_time",
+                {"time": TuyaTimePayload},
+                False,
+                is_manufacturer_specific=False,
+            ),
+        }
+    )
+
+    def handle_mcu_version_response(
+        self, payload: TuyaMCUCluster.MCUVersion
+    ) -> foundation.Status:
+        """Handle MCU version response."""
+        return foundation.Status.SUCCESS
+
+
+(
+    TuyaQuirkBuilder("_TZE204_p3lqqy2r", "TS0601")
+    .tuya_dp(
+        dp_id=1,
+        ep_attribute=TuyaThermostat.ep_attribute,
+        attribute_name=TuyaThermostat.AttributeDefs.system_mode.name,
+        converter=lambda x: 0x00 if not x else 0x04,
+        dp_converter=lambda x: x != 0x00,
+    )
+    .tuya_enum(
+        dp_id=2,
+        attribute_name="preset_mode",
+        enum_class=PresetModeV01,
+        translation_key="preset_mode",
+        fallback_name="Preset mode",
+    )
+    .tuya_dp(
+        dp_id=16,
+        ep_attribute=TuyaThermostat.ep_attribute,
+        attribute_name=TuyaThermostat.AttributeDefs.occupied_heating_setpoint.name,
+        converter=lambda x: x * 100,
+        dp_converter=lambda x: x // 100,
+    )
+    .tuya_dp(
+        dp_id=24,
+        ep_attribute=TuyaThermostat.ep_attribute,
+        attribute_name=TuyaThermostat.AttributeDefs.local_temperature.name,
+        converter=lambda x: x * 100,
+    )
+    .tuya_dp(
+        dp_id=28,
+        ep_attribute=TuyaThermostat.ep_attribute,
+        attribute_name=Thermostat.AttributeDefs.local_temperature_calibration.name,
+        converter=lambda x: x * 100,
+        dp_converter=lambda x: x // 100,
+    )
+    .tuya_switch(
+        dp_id=30,
+        attribute_name="child_lock",
+        translation_key="child_lock",
+        fallback_name="Child lock",
+    )
+    .tuya_sensor(
+        dp_id=101,
+        attribute_name="local_temperature_floor",
+        type=t.int16s,
+        device_class=SensorDeviceClass.TEMPERATURE,
+        state_class=SensorStateClass.MEASUREMENT,
+        unit=UnitOfTemperature.CELSIUS,
+        translation_key="local_temperature_floor",
+        fallback_name="Floor temperature",
+    )
+    .tuya_enum(
+        dp_id=102,
+        attribute_name="temperature_sensor_select",
+        enum_class=SensorMode,
+        translation_key="sensor_mode",
+        fallback_name="Sensor mode",
+    )
+    .tuya_dp(
+        dp_id=104,
+        ep_attribute=TuyaThermostat.ep_attribute,
+        attribute_name=TuyaThermostat.AttributeDefs.running_state.name,
+        converter=lambda x: 0x00 if not x else 0x01,
+    )
+    .tuya_binary_sensor(
+        dp_id=106,
+        attribute_name="window_detection",
+        translation_key="window_detection",
+        fallback_name="Open window detection",
+    )
+    .tuya_dp(
+        dp_id=107,
+        ep_attribute=TuyaThermostat.ep_attribute,
+        attribute_name=TuyaThermostat.AttributeDefs.max_heat_setpoint_limit.name,
+        converter=lambda x: x * 100,
+        dp_converter=lambda x: x // 100,
+    )
+    .tuya_enum(
+        dp_id=108,
+        attribute_name="thermostat_mode",
+        enum_class=ThermostatMode,
+        translation_key="thermostat_mode",
+        fallback_name="Thermostat mode",
+    )
+    .tuya_enum(
+        dp_id=109,
+        attribute_name="regulator_period",
+        enum_class=RegulatorPeriod,
+        translation_key="regulator_period",
+        fallback_name="Regulator period",
+    )
+    .tuya_number(
+        dp_id=110,
+        attribute_name="regulator_set_point",
+        type=t.uint16_t,
+        unit=UnitOfTemperature.CELSIUS,
+        min_value=0,
+        max_value=100,
+        step=1,
+        translation_key="regulator_set_point",
+        fallback_name="Regulator set point",
+    )
+    .adds(TuyaThermostat)
+    .tuya_sensor(
+        dp_id=120,
+        attribute_name="current",
+        type=t.int16s,
+        divisor=10,
+        device_class=SensorDeviceClass.CURRENT,
+        state_class=SensorStateClass.MEASUREMENT,
+        unit=UnitOfElectricCurrent.AMPERE,
+        fallback_name="Current",
+    )
+    .tuya_sensor(
+        dp_id=121,
+        attribute_name="voltage",
+        type=t.int16s,
+        device_class=SensorDeviceClass.VOLTAGE,
+        state_class=SensorStateClass.MEASUREMENT,
+        unit=UnitOfElectricPotential.VOLT,
+        fallback_name="Voltage",
+    )
+    .tuya_sensor(
+        dp_id=122,
+        attribute_name="power",
+        type=t.int16s,
+        device_class=SensorDeviceClass.POWER,
+        state_class=SensorStateClass.MEASUREMENT,
+        unit=UnitOfPower.WATT,
+        fallback_name="Power",
+    )
+    .tuya_sensor(
+        dp_id=123,
+        attribute_name="energy",
+        type=t.int16s,
+        divisor=100,
+        device_class=SensorDeviceClass.ENERGY,
+        state_class=SensorStateClass.TOTAL,
+        unit=UnitOfEnergy.KILO_WATT_HOUR,
+        fallback_name="Energy",
+    )
+    .skip_configuration()
+    .add_to_registry()
+)
+
+
+# Tuya ZWT198/ZWT100-BH Avatto wall thermostat
+base_avatto_quirk = (
+    TuyaQuirkBuilder()
+    .tuya_dp(
+        dp_id=1,
+        ep_attribute=TuyaThermostat.ep_attribute,
+        attribute_name=TuyaThermostat.AttributeDefs.system_mode.name,
+        converter=lambda x: 0x00 if not x else 0x04,
+        dp_converter=lambda x: x != 0x00,
+    )
+    .tuya_dp(
+        dp_id=2,
+        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_dp(
+        dp_id=3,
+        ep_attribute=TuyaThermostat.ep_attribute,
+        attribute_name=TuyaThermostat.AttributeDefs.local_temperature.name,
+        converter=lambda x: x * 10,
+    )
+    .tuya_switch(
+        dp_id=9,
+        attribute_name="child_lock",
+        translation_key="child_lock",
+        fallback_name="Child lock",
+    )
+    .tuya_binary_sensor(
+        dp_id=11,
+        attribute_name="fault_alarm",
+        entity_type=EntityType.STANDARD,
+        device_class=BinarySensorDeviceClass.PROBLEM,
+        translation_key="fault_alarm",
+        fallback_name="Fault alarm",
+    )
+    .tuya_dp(
+        dp_id=15,
+        ep_attribute=TuyaThermostat.ep_attribute,
+        attribute_name=TuyaThermostat.AttributeDefs.max_heat_setpoint_limit.name,
+        converter=lambda x: x * 10,
+        dp_converter=lambda x: x // 10,
+    )
+    .tuya_dp(
+        dp_id=19,
+        ep_attribute=TuyaThermostat.ep_attribute,
+        attribute_name=TuyaThermostat.AttributeDefs.local_temperature_calibration.name,
+        converter=lambda x: x * 10,
+        dp_converter=lambda x: x // 10,
+    )
+    .tuya_dp(
+        dp_id=101,
+        ep_attribute=TuyaThermostat.ep_attribute,
+        attribute_name=TuyaThermostat.AttributeDefs.running_state.name,
+        converter=lambda x: 0x00 if not x else 0x01,
+    )
+    .tuya_switch(
+        dp_id=102,
+        attribute_name="frost_protection",
+        translation_key="frost_protection",
+        fallback_name="Frost protection",
+    )
+    .tuya_switch(
+        dp_id=103,
+        attribute_name="factory_reset",
+        translation_key="factory_reset",
+        fallback_name="Factory reset",
+    )
+    .tuya_enum(
+        dp_id=106,
+        attribute_name="temperature_sensor_select",
+        enum_class=SensorMode,
+        translation_key="sensor_mode",
+        fallback_name="Sensor mode",
+    )
+    .tuya_number(
+        dp_id=107,
+        attribute_name="deadzone_temperature",
+        type=t.uint16_t,
+        unit=UnitOfTemperature.CELSIUS,
+        min_value=0.1,
+        max_value=10,
+        step=0.1,
+        translation_key="deadzone_temperature",
+        fallback_name="Deadzone temperature",
+    )
+    # 109 ZWT198 schedule, skipped
+    .tuya_enum(
+        dp_id=110,
+        attribute_name="backlight_mode",
+        enum_class=BacklightMode,
+        translation_key="backlight_mode",
+        fallback_name="Backlight mode",
+    )
+    .adds(TuyaThermostat)
+    .skip_configuration()
+)
+
+
+(
+    base_avatto_quirk.clone()
+    .applies_to("_TZE204_lzriup1j", "TS0601")
+    .tuya_enum(
+        dp_id=4,
+        attribute_name="preset_mode",
+        enum_class=PresetModeV02,
+        translation_key="preset_mode",
+        fallback_name="Preset mode",
+    )
+    .tuya_enum(
+        dp_id=104,
+        attribute_name="working_day",
+        enum_class=WorkingDayV02,
+        translation_key="working_day",
+        fallback_name="Working day",
+    )
+    .add_to_registry(replacement_cluster=NoManufTimeNoVersionRespTuyaMCUCluster)
+)
+
+
+(
+    base_avatto_quirk.clone()
+    .applies_to("_TZE200_viy9ihs7", "TS0601")
+    .tuya_enum(
+        dp_id=4,
+        attribute_name="preset_mode",
+        enum_class=PresetModeV03,
+        translation_key="preset_mode",
+        fallback_name="Preset mode",
+    )
+    .tuya_enum(
+        dp_id=104,
+        attribute_name="working_day",
+        enum_class=WorkingDayV01,
+        translation_key="working_day",
+        fallback_name="Working day",
+    )
+    .add_to_registry(replacement_cluster=NoManufTimeNoVersionRespTuyaMCUCluster)
+)
+
+
+(
+    base_avatto_quirk.clone()
+    .applies_to("_TZE204_xnbkhhdr", "TS0601")
+    .applies_to("_TZE284_xnbkhhdr", "TS0601")
+    .tuya_enum(
+        dp_id=4,
+        attribute_name="preset_mode",
+        enum_class=PresetModeV03,
+        translation_key="preset_mode",
+        fallback_name="Preset mode",
+    )
+    .tuya_enum(
+        dp_id=104,
+        attribute_name="working_day",
+        enum_class=WorkingDayV02,
+        translation_key="working_day",
+        fallback_name="Working day",
+    )
+    .add_to_registry(replacement_cluster=NoManufTimeNoVersionRespTuyaMCUCluster)
+)


Test patch:
diff --git a/tests/test_tuya_sensor.py b/tests/test_tuya_sensor.py
index 649247a..2628e54 100644
--- a/tests/test_tuya_sensor.py
+++ b/tests/test_tuya_sensor.py
@@ -36,6 +36,7 @@ zhaquirks.setup()
         ("_TZE200_eanjj2pa", "TS0601", 100, 10, False),
         ("_TZE200_ydrdfkim", "TS0601", 100, 10, False),
         ("_TZE284_locansqn", "TS0601", 100, 10, False),
+        ("_TZE200_vvmbj46n", "TS0601", 100, 10, True),
     ],
 )
 async def test_handle_get_data(
diff --git a/tests/test_tuya_thermostat.py b/tests/test_tuya_thermostat.py
index 4818ebf..220d829 100644
--- a/tests/test_tuya_thermostat.py
+++ b/tests/test_tuya_thermostat.py
@@ -6,50 +6,84 @@ from zigpy.zcl.clusters.hvac import Thermostat
 
 from tests.common import ClusterListener
 import zhaquirks
+from zhaquirks.tuya import TUYA_MCU_VERSION_RSP
 from zhaquirks.tuya.mcu import TuyaMCUCluster
 
 zhaquirks.setup()
 
+ZCL_TUYA_VERSION_RSP = b"\x09\x06\x11\x01\x6d\x82"
+ZCL_TUYA_SET_TIME = b"\x09\x12\x24\x0d\x00"
+
 
 @pytest.mark.parametrize(
-    "msg,attr,value",
+    "manuf,msg,attr,value",
     [
         (
+            "_TZE204_p3lqqy2r",
             b"\t\x13\x02\x00\x06\x01\x01\x00\x01\x01",
             Thermostat.AttributeDefs.system_mode,
             Thermostat.SystemMode.Heat,
         ),  # Set to heat, dp 1
         (
+            "_TZE204_p3lqqy2r",
             b"\t\x16\x02\x00\t\x18\x02\x00\x04\x00\x00\x00\x18",
             Thermostat.AttributeDefs.local_temperature,
             2400,
         ),  # Current temp 24, dp 24
         (
+            "_TZE204_p3lqqy2r",
             b"\t\x15\x02\x00\x08\x10\x02\x00\x04\x00\x00\x00\x19",
             Thermostat.AttributeDefs.occupied_heating_setpoint,
             2500,
         ),  # Setpoint to 25, dp 16
         (
+            "_TZE204_p3lqqy2r",
             b"\t\x17\x02\x00\n\x1c\x02\x00\x04\x00\x00\x00\x00",
             Thermostat.AttributeDefs.local_temperature_calibration,
             0,
         ),  # Local calibration to 0, dp 28
         (
+            "_TZE204_p3lqqy2r",
             b"\t\x1c\x02\x00\x0fh\x01\x00\x01\x01",
             Thermostat.AttributeDefs.running_state,
             Thermostat.RunningState.Heat_State_On,
         ),  # Running state, dp 104
         (
+            "_TZE204_p3lqqy2r",
             b"\t\x1d\x02\x00\x10k\x02\x00\x04\x00\x00\x00\x1b",
             Thermostat.AttributeDefs.max_heat_setpoint_limit,
             2700,
         ),  # Max heat set point, dp 107
+        (
+            "_TZE204_lzriup1j",
+            b"\t\x13\x02\x00\x06\x01\x01\x00\x01\x01",
+            Thermostat.AttributeDefs.system_mode,
+            Thermostat.SystemMode.Heat,
+        ),  # Set to heat, dp 1
+        (
+            "_TZE200_viy9ihs7",
+            b"\t\x13\x02\x00\x06\x01\x01\x00\x01\x01",
+            Thermostat.AttributeDefs.system_mode,
+            Thermostat.SystemMode.Heat,
+        ),  # Set to heat, dp 1
+        (
+            "_TZE204_xnbkhhdr",
+            b"\t\x13\x02\x00\x06\x01\x01\x00\x01\x01",
+            Thermostat.AttributeDefs.system_mode,
+            Thermostat.SystemMode.Heat,
+        ),  # Set to heat, dp 1
+        (
+            "_TZE284_xnbkhhdr",
+            b"\t\x13\x02\x00\x06\x01\x01\x00\x01\x01",
+            Thermostat.AttributeDefs.system_mode,
+            Thermostat.SystemMode.Heat,
+        ),  # Set to heat, dp 1
     ],
 )
-async def test_handle_get_data(zigpy_device_from_v2_quirk, msg, attr, value):
+async def test_handle_get_data(zigpy_device_from_v2_quirk, manuf, msg, attr, value):
     """Test handle_get_data for multiple attributes."""
 
-    quirked = zigpy_device_from_v2_quirk("_TZE204_p3lqqy2r", "TS0601")
+    quirked = zigpy_device_from_v2_quirk(manuf, "TS0601")
     ep = quirked.endpoints[1]
 
     assert ep.tuya_manufacturer is not None
@@ -69,3 +103,21 @@ async def test_handle_get_data(zigpy_device_from_v2_quirk, msg, attr, value):
     assert thermostat_listener.attribute_updates[0][1] == value
 
     assert ep.thermostat.get(attr.id) == value
+
+
+async def test_tuya_no_mcu_version(zigpy_device_from_v2_quirk):
+    """Test lack of TUYA_MCU_VERSION_RSP messages."""
+
+    tuya_device = zigpy_device_from_v2_quirk("_TZE284_xnbkhhdr", "TS0601")
+
+    tuya_cluster = tuya_device.endpoints[1].tuya_manufacturer
+    cluster_listener = ClusterListener(tuya_cluster)
+
+    assert len(cluster_listener.attribute_updates) == 0
+
+    # simulate a TUYA_MCU_VERSION_RSP message
+    hdr, args = tuya_cluster.deserialize(ZCL_TUYA_VERSION_RSP)
+    assert hdr.command_id == TUYA_MCU_VERSION_RSP
+
+    tuya_cluster.handle_message(hdr, args)
+    assert len(cluster_listener.attribute_updates) == 0