Repository: zigpy/zha-device-handlers
Pull request #3767 (2025-01-25 07:51:37)

Add Tuya plug TS011F `_TZ3000_okaz9tjs` variant
## Proposed change
<!--
  Explain your proposed change below.
-->
Add quirk for another variation of Tuya Plug TS011F 

## 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".
-->
Device signature
```json
{
  "node_descriptor": {
    "logical_type": 1,
    "complex_descriptor_available": 0,
    "user_descriptor_available": 0,
    "reserved": 0,
    "aps_flags": 0,
    "frequency_band": 8,
    "mac_capability_flags": 142,
    "manufacturer_code": 4417,
    "maximum_buffer_size": 66,
    "maximum_incoming_transfer_size": 66,
    "server_mask": 10752,
    "maximum_outgoing_transfer_size": 66,
    "descriptor_capability_field": 0
  },
  "endpoints": {
    "1": {
      "profile_id": "0x0104",
      "device_type": "0x0100",
      "input_clusters": [
        "0x0000",
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0006",
        "0x0702",
        "0x0b04",
        "0xe000",
        "0xe001"
      ],
      "output_clusters": [
        "0x000a",
        "0x0019"
      ]
    },
    "242": {
      "profile_id": "0xa1e0",
      "device_type": "0x0061",
      "input_clusters": [],
      "output_clusters": [
        "0x0021"
      ]
    }
  },
  "manufacturer": "_TZ3000_okaz9tjs",
  "model": "TS011F"
}
```

## 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
- [ ] Tests have been added to verify that the new code works


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3767?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.40%. Comparing base [(`d9481d3`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/d9481d36a8483810696fe1c0e796895b92d6479b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) to head [(`c8cd905`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/c8cd90536c3147de7a99fc3b3ed0e9680b6ecc26?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    #3767   +/-   ##
=======================================
  Coverage   90.40%   90.40%           
=======================================
  Files         320      320           
  Lines       10490    10494    +4     
=======================================
+ Hits         9483     9487    +4     
  Misses       1007     1007           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3767?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).


Patch:
diff --git a/zhaquirks/lixee/zlinky.py b/zhaquirks/lixee/zlinky.py
index 92a0ccb..2947054 100644
--- a/zhaquirks/lixee/zlinky.py
+++ b/zhaquirks/lixee/zlinky.py
@@ -64,6 +64,8 @@ class ZLinkyTICManufacturerCluster(CustomCluster):
         0x0008: ("hist_current_exceeding_warning_phase_3", t.uint16_t, True),
         # Historical mode: MOTDETAT "Etat du Linky (From V13)" / String 6 car
         0x0009: ("linky_status", t.LimitedCharString(6), True),
+        # Historical and standard mode: "Tariff Period (From V15)" / String 16 car
+        0x0010: ("linky_tariff_period", t.LimitedCharString(16), True),
         # Historical and Standard mode: "Linky acquisition time (From V7)"" / Uint8 1 car
         0x0100: ("linky_acquisition_time", t.uint8_t, True),
         # Standard mode: LTARF "Libellé tarif fournisseur en cours" / String 16 car
@@ -224,3 +226,13 @@ class ZLinkyTICFWV14(ZLinkyTICFWV12):
     signature[ENDPOINTS][1][OUTPUT_CLUSTERS].insert(1, TuyaManufCluster.cluster_id)
 
     replacement[ENDPOINTS][1][INPUT_CLUSTERS].insert(1, Time.cluster_id)
+
+
+class ZLinkyTICFWV15(ZLinkyTICFWV14):
+    """ZLinky_TIC from LiXee with firmware v15.0+."""
+
+    signature = deepcopy(ZLinkyTICFWV14.signature)
+    replacement = deepcopy(ZLinkyTICFWV14.replacement)
+
+    signature[ENDPOINTS][1][DEVICE_TYPE] = zha.DeviceType.DIMMABLE_LIGHT
+    replacement[ENDPOINTS][1][DEVICE_TYPE] = zha.DeviceType.DIMMABLE_LIGHT
diff --git a/zhaquirks/tuya/ts011f_plug.py b/zhaquirks/tuya/ts011f_plug.py
index 24812e2..3db97d7 100644
--- a/zhaquirks/tuya/ts011f_plug.py
+++ b/zhaquirks/tuya/ts011f_plug.py
@@ -1227,6 +1227,74 @@ class Plug_v3_NoGP(EnchantedDevice):
     }
 
 
+class Plug_v4(EnchantedDevice):
+    """Another TS011F Tuya plug. This one using this definition is _TZ3000_okaz9tjs."""
+
+    quirk_id = TUYA_PLUG_ONOFF
+
+    signature = {
+        MODEL: "TS011F",
+        ENDPOINTS: {
+            # "profile_id": "260",
+            # "device_type": "0x0100",
+            # "in_clusters": ["0x0000", "0x0003", "0x0004", "0x0005", "0x0006", "0x0702", "0x0b04", "0xe000", "0xe001"],
+            # "out_clusters": ["0x000a","0x0019"]
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    Metering.cluster_id,
+                    ElectricalMeasurement.cluster_id,
+                    TuyaZBE000Cluster.cluster_id,
+                    TuyaZBExternalSwitchTypeCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            242: {
+                # "profile_id": 41440,
+                # "device_type": "0x0061",
+                # "in_clusters": [],
+                # "out_clusters": ["0x0021"]
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaZBOnOffAttributeCluster,
+                    TuyaZBMeteringClusterWithUnit,
+                    TuyaZBElectricalMeasurement,
+                    TuyaZBE000Cluster,
+                    TuyaZBExternalSwitchTypeCluster,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
+
+
 class Plug_2AC_var03(CustomDevice):
     """Tuya 2 socket wall outlet with child lock and power-restore state support."""
 
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