Repository: zigpy/zha-device-handlers

Issue:
[Device Support Request] Moes BHT-002GCLZB / TS0601 (Thermostat)
**Is your feature request related to a problem? Please describe.**
Device succesfully joins the Zigbee network but no entities are created on HA.

**Describe the solution you'd like**
Please consider adding support for this device.

**Device signature - this can be acquired by removing the device from ZHA and pairing it again from the add devices screen. Be sure to add the entire content of the log panel after pairing the device to a code block below this line.**
```

[0x0000:zdo] ZDO request ZDOCmd.Mgmt_Permit_Joining_req: [60, <Bool.false: 0>]
Device 0xe179 (60:a4:23:ff:fe:b2:7e:8b) joined the network
Device 0xe179 (60:a4:23:ff:fe:b2:7e:8b) joined the network
Skip initialization for existing device 60:a4:23:ff:fe:b2:7e:8b
[0xe179] Requesting 'Node Descriptor'
Tries remaining: 2
[0xe179] Extending timeout for 0x2d request
Device 0xe179 (60:a4:23:ff:fe:b2:7e:8b) joined the network
Skip initialization for existing device 60:a4:23:ff:fe:b2:7e:8b
[0xe179:zdo] ZDO request ZDOCmd.Device_annce: [0xE179, 60:a4:23:ff:fe:b2:7e:8b, 142]
[0xe179] Node Descriptor: NodeDescriptor(byte1=1, byte2=64, mac_capability_flags=142, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=0)
[0xe179] Discovering endpoints
Tries remaining: 3
[0xe179] Discovered endpoints: [1]
[0xe179:1] Discovering endpoint information
Tries remaining: 3
[0xe179:1] Discovered endpoint information: SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=81, device_version=1, input_clusters=[0, 4, 5, 61184], output_clusters=[25, 10])
Unknown cluster 61184
[0xe179:1:0x0000] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=51 command_id=Command.Read_Attributes_rsp>
[0xe179:1] Manufacturer: _TZE200_aoclfnxz
[0xe179:1] Model: TS0601
Checking quirks for _TZE200_aoclfnxz TS0601 (60:a4:23:ff:fe:b2:7e:8b)
Considering <class 'bellows.zigbee.application.EZSPCoordinator'>
Fail because device_type mismatch on at least one endpoint
Considering <class 'zhaquirks.gledopto.soposhgu10.SoposhGU10'>
Fail because endpoint list mismatch: {11, 13} {1}
Considering <class 'zhaquirks.lutron.lzl4bwhl01remote.LutronLZL4BWHL01Remote2'>
Fail because profile_id mismatch on at least one endpoint
Considering <class 'zhaquirks.netvox.z308e3ed.Z308E3ED'>
Fail because device_type mismatch on at least one endpoint
Considering <class 'zhaquirks.osram.a19twhite.A19TunableWhite'>
Fail because endpoint list mismatch: {3} {1}
Considering <class 'zhaquirks.philips.rom001.PhilipsROM001'>
Fail because device_type mismatch on at least one endpoint
Considering <class 'zhaquirks.philips.rwl020.PhilipsRWL020'>
Fail because endpoint list mismatch: {1, 2} {1}
Considering <class 'zhaquirks.philips.rwl021.PhilipsRWL021'>
Fail because endpoint list mismatch: {1, 2} {1}
Considering <class 'zhaquirks.smartthings.multi.SmartthingsMultiPurposeSensor'>
Fail because device_type mismatch on at least one endpoint
Considering <class 'zhaquirks.smartthings.tag_v4.SmartThingsTagV4'>
Fail because device_type mismatch on at least one endpoint
Considering <class 'zhaquirks.xbee.xbee3_io.XBee3Sensor'>
Fail because endpoint list mismatch: {232, 230} {1}
Considering <class 'zhaquirks.xbee.xbee_io.XBeeSensor'>
Fail because endpoint list mismatch: {232, 230} {1}
Considering <class 'zhaquirks.xiaomi.mija.smoke.MijiaHoneywellSmokeDetectorSensor'>
Fail because device_type mismatch on at least one endpoint
device - 0xE179:60:a4:23:ff:fe:b2:7e:8b entering async_device_initialized - is_new_join: True
device - 0xE179:60:a4:23:ff:fe:b2:7e:8b has joined the ZHA zigbee network
[0xE179](TS0601): started configuration
[0xE179:ZDO](TS0601): 'async_configure' stage succeeded
[0xE179:1:0x0019]: bound 'ota' cluster: Status.SUCCESS
[0xE179:1:0x0019]: finished channel configuration
[0xE179:1:0x0000]: bound 'basic' cluster: Status.SUCCESS
[0xE179:1:0x0000]: finished channel configuration
[0xe179:1:0x0000] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=57 command_id=Command.Read_Attributes_rsp>
[0xE179:1:0x0000]: initializing channel: from_cache: False
[0xE179:1:0x0000]: 'async_configure' stage succeeded
[0xE179:1:0x0019]: 'async_configure' stage succeeded
[0xE179](TS0601): completed configuration
[0xE179](TS0601): stored in registry: ZhaDeviceEntry(name='_TZE200_aoclfnxz TS0601', ieee='60:a4:23:ff:fe:b2:7e:8b', last_seen=1604351812.0728118)
[0xE179](TS0601): started initialization
[0xE179:ZDO](TS0601): 'async_initialize' stage succeeded
[0xE179:1:0x0019]: initializing channel: from_cache: False
[0xe179:1:0x0000] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=59 command_id=Command.Read_Attributes_rsp>
[0xE179:1:0x0000]: initializing channel: from_cache: False
[0xE179:1:0x0000]: 'async_initialize' stage succeeded
[0xE179:1:0x0019]: 'async_initialize' stage succeeded
[0xE179](TS0601): power source: Mains
[0xE179](TS0601): completed initialization
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=0 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x06$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=0 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x06$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=0 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x06$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=0 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x06$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=0 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x06$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=1 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x07$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=2 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x08$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=3 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\t$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:zdo] ZDO request ZDOCmd.Node_Desc_req: [0x0000]
[0xe179:zdo] Unsupported ZDO request:ZDOCmd.Node_Desc_req
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=4 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\n$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=5 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x0b$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=6 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x0c$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=7 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\r$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=8 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x0e$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=9 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x0f$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=10 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x10$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=11 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x11$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=12 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x12$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=13 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x13$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=14 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x14$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=15 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x15$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=16 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x16$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=17 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x17$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=18 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x18$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=19 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x19$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=20 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x1a$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=21 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x1b$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=22 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x1c$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=23 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x1d$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=24 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x1e$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=25 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00\x1f$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=26 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00 $\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=27 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00!$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=28 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00"$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=29 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00#$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=False> manufacturer=None tsn=30 command_id=1>
[0xe179:1:0xef00] Unknown cluster-specific command 1
[0xe179:1:0xef00] ZCL request 0x0001: b'\x00$$\x04\x00\x01\x01'
[0xe179:1:0xef00] No handler for cluster command 1
[0xe179:1:0xef00] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=0 command_id=36>
[0xe179:1:0xef00] Unknown cluster-specific command 36
[0xe179:1:0xef00] ZCL request 0x0024: b'\x00\x03'
[0xe179:1:0xef00] No handler for cluster command 36
[0xe179:1:0x000a] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=False disable_default_response=False> manufacturer=None tsn=1 command_id=Command.Read_Attributes>
[0xe179:1:0x000a] ZCL request 0x0000: [[7]]
```

**Additional context**
Device signature:
```
{
  "node_descriptor": "NodeDescriptor(byte1=1, byte2=64, mac_capability_flags=142, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=0)",
  "endpoints": {
    "1": {
      "profile_id": 260,
      "device_type": "0x0051",
      "in_clusters": [
        "0x0000",
        "0x0004",
        "0x0005",
        "0xef00"
      ],
      "out_clusters": [
        "0x000a",
        "0x0019"
      ]
    }
  },
  "manufacturer": "_TZE200_aoclfnxz",
  "model": "TS0601",
  "class": "zigpy.device.Device"
}
```

This is the actual device:
![imagen](https://user-images.githubusercontent.com/16177130/97921382-2aaad400-1d5b-11eb-8599-018cbe085061.png)




Discussion:
take a look at https://github.com/zigpy/zha-device-handlers/pull/538
Work with @ FrnchFrgg help with testing and feedback. 

Resolving patch:
diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py
--- a/zhaquirks/tuya/__init__.py
+++ b/zhaquirks/tuya/__init__.py
@@ -5,9 +5,10 @@
 from zigpy.quirks import CustomCluster, CustomDevice
 import zigpy.types as t
 from zigpy.zcl import foundation
-from zigpy.zcl.clusters.general import OnOff
+from zigpy.zcl.clusters.general import OnOff, PowerConfiguration
+from zigpy.zcl.clusters.hvac import Thermostat, UserInterface
 
-from .. import Bus
+from .. import Bus, LocalDataCluster
 
 TUYA_CLUSTER_ID = 0xEF00
 TUYA_SET_DATA = 0x0000
@@ -24,6 +25,23 @@
 class Data(t.List, item_type=t.uint8_t):
     """list of uint8_t."""
 
+    @classmethod
+    def from_value(cls, value):
+        """Convert from a zigpy typed value to a tuya data payload."""
+        # serialized in little-endian by zigpy
+        data = cls(value.serialize())
+        # we want big-endian, with length prepended
+        data.append(len(data))
+        data.reverse()
+        return data
+
+    def to_value(self, ztype):
+        """Convert from a tuya data payload to a zigpy typed value."""
+        # first uint8_t is the length of the remaining data
+        # tuya data is in big endian whereas ztypes use little endian
+        value, _ = ztype.deserialize(bytes(reversed(self[1:])))
+        return value
+
 
 class TuyaManufCluster(CustomCluster):
     """Tuya manufacturer specific cluster."""
@@ -58,7 +76,7 @@ def handle_cluster_request(self, tsn: int, command_id: int, args: Tuple) -> None
             return super().handle_cluster_request(tsn, command_id, args)
 
         tuya_cmd = args[0].command_id
-        tuya_value = args[0].data[1:]  # first uint8_t is length
+        tuya_data = args[0].data
 
         _LOGGER.debug(
             "[0x%04x:%s:0x%04x] Received value %s "
@@ -66,7 +84,7 @@ def handle_cluster_request(self, tsn: int, command_id: int, args: Tuple) -> None
             self.endpoint.device.nwk,
             self.endpoint.endpoint_id,
             self.cluster_id,
-            repr(tuya_value),
+            repr(tuya_data[1:]),
             tuya_cmd,
             command_id,
         )
@@ -74,9 +92,8 @@ def handle_cluster_request(self, tsn: int, command_id: int, args: Tuple) -> None
         if tuya_cmd not in self.attributes:
             return
 
-        # tuya data is in big endian whereas ztypes use little endian
         ztype = self.attributes[tuya_cmd][1]
-        zvalue, _ = ztype.deserialize(bytes(reversed(tuya_value)))
+        zvalue = tuya_data.to_value(ztype)
         self._update_attribute(tuya_cmd, zvalue)
 
     def read_attributes(
@@ -94,18 +111,12 @@ async def write_attributes(self, attributes, manufacturer=None):
         records = self._write_attr_records(attributes)
 
         for record in records:
-            # serialized in little-endian
-            data = list(record.value.value.serialize())
-            # we want big-endian, with length prepended
-            data.append(len(data))
-            data.reverse()
-
             cmd_payload = TuyaManufCluster.Command()
             cmd_payload.status = 0
             cmd_payload.tsn = self.endpoint.device.application.get_sequence()
             cmd_payload.command_id = record.attrid
             cmd_payload.function = 0
-            cmd_payload.data = data
+            cmd_payload.data = Data.from_value(record.value.value)
 
             await super().command(
                 TUYA_SET_DATA,
@@ -185,3 +196,158 @@ def __init__(self, *args, **kwargs):
         """Init device."""
         self.switch_bus = Bus()
         super().__init__(*args, **kwargs)
+
+
+class TuyaThermostatCluster(LocalDataCluster, Thermostat):
+    """Thermostat cluster for Tuya thermostats."""
+
+    _CONSTANT_ATTRIBUTES = {0x001B: Thermostat.ControlSequenceOfOperation.Heating_Only}
+
+    def __init__(self, *args, **kwargs):
+        """Init."""
+        super().__init__(*args, **kwargs)
+        self.endpoint.device.thermostat_bus.add_listener(self)
+
+    def temperature_change(self, attr, value):
+        """Local or target temperature change from device."""
+        self._update_attribute(self.attridx[attr], value)
+
+    def state_change(self, value):
+        """State update from device."""
+        if value == 0:
+            mode = self.RunningMode.Off
+            state = self.RunningState.Idle
+        else:
+            mode = self.RunningMode.Heat
+            state = self.RunningState.Heat_State_On
+        self._update_attribute(self.attridx["running_mode"], mode)
+        self._update_attribute(self.attridx["running_state"], state)
+
+    # pylint: disable=R0201
+    def map_attribute(self, attribute, value):
+        """Map standardized attribute value to dict of manufacturer values."""
+        return {}
+
+    async def write_attributes(self, attributes, manufacturer=None):
+        """Implement writeable attributes."""
+
+        records = self._write_attr_records(attributes)
+
+        if not records:
+            return (foundation.Status.SUCCESS,)
+
+        manufacturer_attrs = {}
+        for record in records:
+            attr_name = self.attributes[record.attrid][0]
+            new_attrs = self.map_attribute(attr_name, record.value.value)
+
+            _LOGGER.debug(
+                "[0x%04x:%s:0x%04x] Mapping standard %s (0x%04x) "
+                "with value %s to custom %s",
+                self.endpoint.device.nwk,
+                self.endpoint.endpoint_id,
+                self.cluster_id,
+                attr_name,
+                record.attrid,
+                repr(record.value.value),
+                repr(new_attrs),
+            )
+
+            manufacturer_attrs.update(new_attrs)
+
+        if not manufacturer_attrs:
+            return (foundation.Status.FAILURE,)
+
+        await self.endpoint.tuya_manufacturer.write_attributes(
+            manufacturer_attrs, manufacturer=manufacturer
+        )
+
+        return (foundation.Status.SUCCESS,)
+
+    # pylint: disable=W0236
+    async def command(
+        self,
+        command_id: Union[foundation.Command, int, t.uint8_t],
+        *args,
+        manufacturer: Optional[Union[int, t.uint16_t]] = None,
+        expect_reply: bool = True,
+        tsn: Optional[Union[int, t.uint8_t]] = None,
+    ):
+        """Implement thermostat commands."""
+
+        if command_id != 0x0000:
+            return foundation.Status.UNSUP_CLUSTER_COMMAND
+
+        mode, offset = args
+        if mode not in (self.SetpointMode.Heat, self.SetpointMode.Both):
+            return foundation.Status.INVALID_VALUE
+
+        attrid = self.attridx["occupied_heating_setpoint"]
+
+        success, _ = await self.read_attributes((attrid,), manufacturer=manufacturer)
+        try:
+            current = success[attrid]
+        except KeyError:
+            return foundation.Status.FAILURE
+
+        # offset is given in decidegrees, see Zigbee cluster specification
+        return await self.write_attributes(
+            {"occupied_heating_setpoint": current + offset * 10},
+            manufacturer=manufacturer,
+        )
+
+
+class TuyaUserInterfaceCluster(LocalDataCluster, UserInterface):
+    """HVAC User interface cluster for tuya thermostats."""
+
+    def __init__(self, *args, **kwargs):
+        """Init."""
+        super().__init__(*args, **kwargs)
+        self.endpoint.device.ui_bus.add_listener(self)
+
+    def child_lock_change(self, mode):
+        """Change of child lock setting."""
+        if mode == 0:
+            lockout = self.KeypadLockout.No_lockout
+        else:
+            lockout = self.KeypadLockout.Level_1_lockout
+
+        self._update_attribute(self.attridx["keypad_lockout"], lockout)
+
+    async def write_attributes(self, attributes, manufacturer=None):
+        """Defer the keypad_lockout attribute to child_lock."""
+
+        records = self._write_attr_records(attributes)
+
+        for record in records:
+            if record.attrid == self.attridx["keypad_lockout"]:
+                lock = 0 if record.value.value == self.KeypadLockout.No_lockout else 1
+                return await self.endpoint.tuya_manufacturer.write_attributes(
+                    {self._CHILD_LOCK_ATTR: lock}, manufacturer=manufacturer
+                )
+
+        return (foundation.Status.FAILURE,)
+
+
+class TuyaPowerConfigurationCluster(LocalDataCluster, PowerConfiguration):
+    """PowerConfiguration cluster for battery-operated thermostats."""
+
+    def __init__(self, *args, **kwargs):
+        """Init."""
+        super().__init__(*args, **kwargs)
+        self.endpoint.device.battery_bus.add_listener(self)
+
+    def battery_change(self, value):
+        """Change of reported battery percentage remaining."""
+        self._update_attribute(self.attridx["battery_percentage_remaining"], value * 2)
+
+
+class TuyaThermostat(CustomDevice):
+    """Generic Tuya thermostat device."""
+
+    def __init__(self, *args, **kwargs):
+        """Init device."""
+        self.thermostat_bus = Bus()
+        self.ui_bus = Bus()
+        self.battery_bus = Bus()
+        super().__init__(*args, **kwargs)
diff --git a/zhaquirks/tuya/electric_heating.py b/zhaquirks/tuya/electric_heating.py
new file mode 100644
--- /dev/null
+++ b/zhaquirks/tuya/electric_heating.py
@@ -0,0 +1,172 @@
+"""Map from manufacturer to standard clusters for electric heating thermostats."""
+import logging
+
+from zigpy.profiles import zha
+import zigpy.types as t
+from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time
+
+from . import (
+    TuyaManufClusterAttributes,
+    TuyaThermostat,
+    TuyaThermostatCluster,
+    TuyaUserInterfaceCluster,
+)
+from ..const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+
+# info from https://github.com/zigpy/zha-device-handlers/pull/538#issuecomment-723334124
+# https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/fromZigbee.js#L239
+# and https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/common.js#L113
+MOESBHT_TARGET_TEMP_ATTR = 0x0210  # [0,0,0,21] target room temp (degree)
+MOESBHT_TEMPERATURE_ATTR = 0x0218  # [0,0,0,200] current room temp (decidegree)
+MOESBHT_SCHEDULE_MODE_ATTR = 0x0403  # [1] false [0] true   /!\ inverted
+MOESBHT_MANUAL_MODE_ATTR = 0x0402  # [1] false [0] true /!\ inverted
+MOESBHT_ENABLED_ATTR = 0x0101  # [0] off [1] on
+MOESBHT_RUNNING_MODE_ATTR = 0x0424  # [1] idle [0] heating /!\ inverted
+MOESBHT_CHILD_LOCK_ATTR = 0x0128  # [0] unlocked [1] child-locked
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class MoesBHTManufCluster(TuyaManufClusterAttributes):
+    """Manufacturer Specific Cluster of some electric heating thermostats."""
+
+    manufacturer_attributes = {
+        MOESBHT_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t),
+        MOESBHT_TEMPERATURE_ATTR: ("temperature", t.uint32_t),
+        MOESBHT_SCHEDULE_MODE_ATTR: ("schedule_mode", t.uint8_t),
+        MOESBHT_MANUAL_MODE_ATTR: ("manual_mode", t.uint8_t),
+        MOESBHT_ENABLED_ATTR: ("enabled", t.uint8_t),
+        MOESBHT_RUNNING_MODE_ATTR: ("running_mode", t.uint8_t),
+        MOESBHT_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t),
+    }
+
+    def _update_attribute(self, attrid, value):
+        super()._update_attribute(attrid, value)
+        if attrid == MOESBHT_TARGET_TEMP_ATTR:
+            self.endpoint.device.thermostat_bus.listener_event(
+                "temperature_change",
+                "occupied_heating_setpoint",
+                value * 100,  # degree to centidegree
+            )
+        elif attrid == MOESBHT_TEMPERATURE_ATTR:
+            self.endpoint.device.thermostat_bus.listener_event(
+                "temperature_change",
+                "local_temp",
+                value * 10,  # decidegree to centidegree
+            )
+        elif attrid == MOESBHT_SCHEDULE_MODE_ATTR:
+            if value == 0:  # value is inverted
+                self.endpoint.device.thermostat_bus.listener_event(
+                    "program_change", "scheduled"
+                )
+        elif attrid == MOESBHT_MANUAL_MODE_ATTR:
+            if value == 0:  # value is inverted
+                self.endpoint.device.thermostat_bus.listener_event(
+                    "program_change", "manual"
+                )
+        elif attrid == MOESBHT_ENABLED_ATTR:
+            self.endpoint.device.thermostat_bus.listener_event("enabled_change", value)
+        elif attrid == MOESBHT_RUNNING_MODE_ATTR:
+            # value is inverted
+            self.endpoint.device.thermostat_bus.listener_event(
+                "state_change", 1 - value
+            )
+        elif attrid == MOESBHT_CHILD_LOCK_ATTR:
+            self.endpoint.device.ui_bus.listener_event("child_lock_change", value)
+
+
+class MoesBHTThermostat(TuyaThermostatCluster):
+    """Thermostat cluster for some electric heating controllers."""
+
+    def map_attribute(self, attribute, value):
+        """Map standardized attribute value to dict of manufacturer values."""
+
+        if attribute == "occupied_heating_setpoint":
+            # centidegree to degree
+            return {MOESBHT_TARGET_TEMP_ATTR: round(value / 100)}
+        if attribute == "system_mode":
+            if value == self.SystemMode.Off:
+                return {MOESBHT_ENABLED_ATTR: 0}
+            if value == self.SystemMode.Heat:
+                return {MOESBHT_ENABLED_ATTR: 1}
+            self.error("Unsupported value for SystemMode")
+        elif attribute == "programing_oper_mode":
+            # values are inverted
+            if value == self.ProgrammingOperationMode.Simple:
+                return {MOESBHT_MANUAL_MODE_ATTR: 0, MOESBHT_SCHEDULE_MODE_ATTR: 1}
+            if value == self.ProgrammingOperationMode.Schedule_programming_mode:
+                return {MOESBHT_MANUAL_MODE_ATTR: 1, MOESBHT_SCHEDULE_MODE_ATTR: 0}
+            self.error("Unsupported value for ProgrammingOperationMode")
+
+        return super().map_attribute(attribute, value)
+
+    def program_change(self, mode):
+        """Programming mode change."""
+        if mode == "manual":
+            value = self.ProgrammingOperationMode.Simple
+        else:
+            value = self.ProgrammingOperationMode.Schedule_programming_mode
+
+        self._update_attribute(self.attridx["programing_oper_mode"], value)
+
+    def enabled_change(self, value):
+        """System mode change."""
+        if value == 0:
+            mode = self.SystemMode.Off
+        else:
+            mode = self.SystemMode.Heat
+        self._update_attribute(self.attridx["system_mode"], mode)
+
+
+class MoesBHTUserInterface(TuyaUserInterfaceCluster):
+    """HVAC User interface cluster for tuya electric heating thermostats."""
+
+    _CHILD_LOCK_ATTR = MOESBHT_CHILD_LOCK_ATTR
+
+
+class MoesBHT(TuyaThermostat):
+    """Moes BHT-002GCLZB Thermostatic radiator valve."""
+
+    signature = {
+        #  endpoint=1 profile=260 device_type=81 device_version=1 input_clusters=[0, 4, 5, 61184],
+        #  output_clusters=[10, 25]
+        MODELS_INFO: [("_TZE200_aoclfnxz", "TS0601")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaManufClusterAttributes.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.THERMOSTAT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    MoesBHTManufCluster,
+                    MoesBHTThermostat,
+                    MoesBHTUserInterface,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            }
+        }
+    }
diff --git a/zhaquirks/tuya/valve.py b/zhaquirks/tuya/valve.py
new file mode 100644
--- /dev/null
+++ b/zhaquirks/tuya/valve.py
@@ -0,0 +1,207 @@
+"""Map from manufacturer to standard clusters for thermostatic valves."""
+import logging
+
+from zigpy.profiles import zha
+import zigpy.types as t
+from zigpy.zcl.clusters.general import Basic, Groups, Identify, Ota, Scenes, Time
+
+from . import (
+    TuyaManufClusterAttributes,
+    TuyaPowerConfigurationCluster,
+    TuyaThermostat,
+    TuyaThermostatCluster,
+    TuyaUserInterfaceCluster,
+)
+from ..const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+
+# info from https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/common.js#L113
+# and https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/fromZigbee.js#L362
+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
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class SiterwellManufCluster(TuyaManufClusterAttributes):
+    """Manufacturer Specific Cluster of some thermostatic valves."""
+
+    manufacturer_attributes = {
+        SITERWELL_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t),
+        SITERWELL_WINDOW_DETECT_ATTR: ("window_detection", t.uint8_t),
+        SITERWELL_VALVE_DETECT_ATTR: ("valve_detect", t.uint8_t),
+        SITERWELL_VALVE_STATE_ATTR: ("valve_state", t.uint32_t),
+        SITERWELL_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t),
+        SITERWELL_TEMPERATURE_ATTR: ("temperature", t.uint32_t),
+        SITERWELL_BATTERY_ATTR: ("battery", t.uint32_t),
+        SITERWELL_MODE_ATTR: ("mode", t.uint8_t),
+    }
+
+    TEMPERATURE_ATTRS = {
+        SITERWELL_TEMPERATURE_ATTR: "local_temp",
+        SITERWELL_TARGET_TEMP_ATTR: "occupied_heating_setpoint",
+    }
+
+    def _update_attribute(self, attrid, value):
+        super()._update_attribute(attrid, value)
+        if attrid in self.TEMPERATURE_ATTRS:
+            self.endpoint.device.thermostat_bus.listener_event(
+                "temperature_change",
+                self.TEMPERATURE_ATTRS[attrid],
+                value * 10,  # decidegree to centidegree
+            )
+        elif attrid == SITERWELL_MODE_ATTR:
+            self.endpoint.device.thermostat_bus.listener_event("mode_change", value)
+            self.endpoint.device.thermostat_bus.listener_event(
+                "state_change", value > 0
+            )
+        elif attrid == SITERWELL_VALVE_STATE_ATTR:
+            self.endpoint.device.thermostat_bus.listener_event("state_change", value)
+        elif attrid == SITERWELL_CHILD_LOCK_ATTR:
+            mode = 1 if value else 0
+            self.endpoint.device.ui_bus.listener_event("child_lock_change", mode)
+        elif attrid == SITERWELL_BATTERY_ATTR:
+            self.endpoint.device.battery_bus.listener_event("battery_change", value)
+
+
+class SiterwellThermostat(TuyaThermostatCluster):
+    """Thermostat cluster for some thermostatic valves."""
+
+    def map_attribute(self, attribute, value):
+        """Map standardized attribute value to dict of manufacturer values."""
+
+        if attribute == "occupied_heating_setpoint":
+            # centidegree to decidegree
+            return {SITERWELL_TARGET_TEMP_ATTR: round(value / 10)}
+        if attribute in ("system_mode", "programing_oper_mode"):
+            if attribute == "system_mode":
+                system_mode = value
+                oper_mode = self._attr_cache.get(
+                    self.attridx["programing_oper_mode"],
+                    self.ProgrammingOperationMode.Simple,
+                )
+            else:
+                system_mode = self._attr_cache.get(
+                    self.attridx["system_mode"], self.SystemMode.Heat
+                )
+                oper_mode = value
+            if system_mode == self.SystemMode.Off:
+                return {SITERWELL_MODE_ATTR: 0}
+            if system_mode == self.SystemMode.Heat:
+                if oper_mode == self.ProgrammingOperationMode.Schedule_programming_mode:
+                    return {SITERWELL_MODE_ATTR: 1}
+                if oper_mode == self.ProgrammingOperationMode.Simple:
+                    return {SITERWELL_MODE_ATTR: 2}
+                self.error("Unsupported value for ProgrammingOperationMode")
+            else:
+                self.error("Unsupported value for SystemMode")
+
+    def mode_change(self, value):
+        """System Mode change."""
+        if value == 0:
+            self._update_attribute(self.attridx["system_mode"], self.SystemMode.Off)
+            return
+
+        if value == 1:
+            mode = self.ProgrammingOperationMode.Schedule_programming_mode
+        else:
+            mode = self.ProgrammingOperationMode.Simple
+
+        self._update_attribute(self.attridx["system_mode"], self.SystemMode.Heat)
+        self._update_attribute(self.attridx["programing_oper_mode"], mode)
+
+
+class SiterwellUserInterface(TuyaUserInterfaceCluster):
+    """HVAC User interface cluster for tuya electric heating thermostats."""
+
+    _CHILD_LOCK_ATTR = SITERWELL_CHILD_LOCK_ATTR
+
+
+class SiterwellGS361(TuyaThermostat):
+    """SiterwellGS361 Thermostatic radiator valve and clones."""
+
+    signature = {
+        #  endpoint=1 profile=260 device_type=0 device_version=0 input_clusters=[0, 3]
+        #  output_clusters=[3, 25]>
+        MODELS_INFO: [("_TYST11_jeaxp72v", "eaxp72v"), ("_TYST11_kfvq6avy", "fvq6avy")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [Basic.cluster_id, Identify.cluster_id],
+                OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.THERMOSTAT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    SiterwellManufCluster,
+                    SiterwellThermostat,
+                    SiterwellUserInterface,
+                    TuyaPowerConfigurationCluster,
+                ],
+                OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id],
+            }
+        }
+    }
+
+
+class MoesHY368(TuyaThermostat):
+    """MoesHY368 Thermostatic radiator valve."""
+
+    signature = {
+        #  endpoint=1 profile=260 device_type=81 device_version=0 input_clusters=[0, 4, 5, 61184]
+        #  output_clusters=[10, 25]>
+        MODELS_INFO: [("_TZE200_ckud7u2l", "TS0601")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaManufClusterAttributes.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.THERMOSTAT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    SiterwellManufCluster,
+                    SiterwellThermostat,
+                    SiterwellUserInterface,
+                    TuyaPowerConfigurationCluster,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            }
+        }
+    }


Test patch:
diff --git a/tests/test_tuya.py b/tests/test_tuya.py
--- a/tests/test_tuya.py
+++ b/tests/test_tuya.py
@@ -4,16 +4,45 @@
 from unittest import mock
 
 import pytest
+from zigpy.profiles import zha
+from zigpy.quirks import CustomDevice
+import zigpy.types as t
 from zigpy.zcl import foundation
 
-from zhaquirks.const import OFF, ON, ZONE_STATE
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OFF,
+    ON,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+    ZONE_STATE,
+)
+from zhaquirks.tuya import Data, TuyaManufClusterAttributes
+import zhaquirks.tuya.electric_heating
 import zhaquirks.tuya.motion
+import zhaquirks.tuya.siren
+import zhaquirks.tuya.valve
 
 from tests.common import ClusterListener
 
 ZCL_TUYA_MOTION = b"\tL\x01\x00\x05\x03\x04\x00\x01\x02"
 ZCL_TUYA_SWITCH_ON = b"\tQ\x02\x006\x01\x01\x00\x01\x01"
 ZCL_TUYA_SWITCH_OFF = b"\tQ\x02\x006\x01\x01\x00\x01\x00"
+ZCL_TUYA_ATTRIBUTE_617_TO_179 = b"\tp\x02\x00\x02i\x02\x00\x04\x00\x00\x00\xb3"
+ZCL_TUYA_SIREN_TEMPERATURE = ZCL_TUYA_ATTRIBUTE_617_TO_179
+ZCL_TUYA_SIREN_HUMIDITY = b"\tp\x02\x00\x02j\x02\x00\x04\x00\x00\x00U"
+ZCL_TUYA_SIREN_ON = b"\t\t\x02\x00\x04h\x01\x00\x01\x01"
+ZCL_TUYA_SIREN_OFF = b"\t\t\x02\x00\x04h\x01\x00\x01\x00"
+ZCL_TUYA_VALVE_TEMPERATURE = b"\tp\x02\x00\x02\x03\x02\x00\x04\x00\x00\x00\xb3"
+ZCL_TUYA_VALVE_TARGET_TEMP = b"\t3\x01\x03\x05\x02\x02\x00\x04\x00\x00\x002"
+ZCL_TUYA_VALVE_OFF = b"\t2\x01\x03\x04\x04\x04\x00\x01\x00"
+ZCL_TUYA_VALVE_SCHEDULE = b"\t2\x01\x03\x04\x04\x04\x00\x01\x01"
+ZCL_TUYA_VALVE_MANUAL = b"\t2\x01\x03\x04\x04\x04\x00\x01\x02"
+ZCL_TUYA_EHEAT_TEMPERATURE = b"\tp\x02\x00\x02\x18\x02\x00\x04\x00\x00\x00\xb3"
+ZCL_TUYA_EHEAT_TARGET_TEMP = b"\t3\x01\x03\x05\x10\x02\x00\x04\x00\x00\x00\x15"
 
 
 @pytest.mark.parametrize("quirk", (zhaquirks.tuya.motion.TuyaMotion,))
@@ -103,3 +132,399 @@ async def test_singleswitch_requests(zigpy_device_from_quirk, quirk):
 
     status = switch_cluster.command(0x0002)
     assert status == foundation.Status.UNSUP_CLUSTER_COMMAND
+
+
+async def test_tuya_data_conversion():
+    """Test tuya conversion from Data to ztype and reverse."""
+    assert Data([4, 0, 0, 1, 39]).to_value(t.uint32_t) == 295
+    assert Data([4, 0, 0, 0, 220]).to_value(t.uint32_t) == 220
+    assert Data([4, 255, 255, 255, 236]).to_value(t.int32s) == -20
+    assert Data.from_value(t.uint32_t(295)) == [4, 0, 0, 1, 39]
+    assert Data.from_value(t.uint32_t(220)) == [4, 0, 0, 0, 220]
+    assert Data.from_value(t.int32s(-20)) == [4, 255, 255, 255, 236]
+
+
+class TestManufCluster(TuyaManufClusterAttributes):
+    """Cluster for synthetic tests."""
+
+    manufacturer_attributes = {617: ("test_attribute", t.uint32_t)}
+
+
+class TestDevice(CustomDevice):
+    """Device for synthetic tests."""
+
+    signature = {
+        MODELS_INFO: [("_test_manuf", "_test_device")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [TestManufCluster.cluster_id],
+                OUTPUT_CLUSTERS: [],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [TestManufCluster],
+                OUTPUT_CLUSTERS: [],
+            }
+        },
+    }
+
+
+@pytest.mark.parametrize("quirk", (TestDevice,))
+async def test_tuya_receive_attribute(zigpy_device_from_quirk, quirk):
+    """Test conversion of tuya commands to attributes."""
+
+    test_dev = zigpy_device_from_quirk(quirk)
+    tuya_cluster = test_dev.endpoints[1].tuya_manufacturer
+    listener = ClusterListener(tuya_cluster)
+
+    hdr, args = tuya_cluster.deserialize(ZCL_TUYA_ATTRIBUTE_617_TO_179)
+    tuya_cluster.handle_message(hdr, args)
+
+    assert len(listener.attribute_updates) == 1
+    assert listener.attribute_updates[0][0] == 617
+    assert listener.attribute_updates[0][1] == 179
+
+
+@pytest.mark.parametrize("quirk", (TestDevice,))
+async def test_tuya_send_attribute(zigpy_device_from_quirk, quirk):
+    """Test conversion of attributes to tuya commands."""
+
+    test_dev = zigpy_device_from_quirk(quirk)
+    tuya_cluster = test_dev.endpoints[1].tuya_manufacturer
+
+    async def async_success(*args, **kwargs):
+        return foundation.Status.SUCCESS
+
+    with mock.patch.object(
+        tuya_cluster.endpoint, "request", side_effect=async_success
+    ) as m1:
+
+        status = await tuya_cluster.write_attributes({617: 179})
+        m1.assert_called_with(
+            61184,
+            1,
+            b"\x01\x01\x00\x00\x01i\x02\x00\x04\x00\x00\x00\xb3",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+
+@pytest.mark.parametrize("quirk", (zhaquirks.tuya.siren.TuyaSiren,))
+async def test_siren_state_report(zigpy_device_from_quirk, quirk):
+    """Test tuya siren standard state reporting from incoming commands."""
+
+    siren_dev = zigpy_device_from_quirk(quirk)
+    tuya_cluster = siren_dev.endpoints[1].tuya_manufacturer
+
+    temp_listener = ClusterListener(siren_dev.endpoints[1].temperature)
+    humid_listener = ClusterListener(siren_dev.endpoints[1].humidity)
+    switch_listener = ClusterListener(siren_dev.endpoints[1].on_off)
+
+    frames = (
+        ZCL_TUYA_SIREN_TEMPERATURE,
+        ZCL_TUYA_SIREN_HUMIDITY,
+        ZCL_TUYA_SIREN_ON,
+        ZCL_TUYA_SIREN_OFF,
+    )
+    for frame in frames:
+        hdr, args = tuya_cluster.deserialize(frame)
+        tuya_cluster.handle_message(hdr, args)
+
+    assert len(temp_listener.cluster_commands) == 0
+    assert len(temp_listener.attribute_updates) == 1
+    assert temp_listener.attribute_updates[0][0] == 0x0000
+    assert temp_listener.attribute_updates[0][1] == 1790
+
+    assert len(humid_listener.cluster_commands) == 0
+    assert len(humid_listener.attribute_updates) == 1
+    assert humid_listener.attribute_updates[0][0] == 0x0000
+    assert humid_listener.attribute_updates[0][1] == 8500
+
+    assert len(switch_listener.cluster_commands) == 0
+    assert len(switch_listener.attribute_updates) == 2
+    assert switch_listener.attribute_updates[0][0] == 0x0000
+    assert switch_listener.attribute_updates[0][1] == ON
+    assert switch_listener.attribute_updates[1][0] == 0x0000
+    assert switch_listener.attribute_updates[1][1] == OFF
+
+
+@pytest.mark.parametrize("quirk", (zhaquirks.tuya.siren.TuyaSiren,))
+async def test_siren_send_attribute(zigpy_device_from_quirk, quirk):
+    """Test tuya siren outgoing commands."""
+
+    siren_dev = zigpy_device_from_quirk(quirk)
+    tuya_cluster = siren_dev.endpoints[1].tuya_manufacturer
+    switch_cluster = siren_dev.endpoints[1].on_off
+
+    async def async_success(*args, **kwargs):
+        return foundation.Status.SUCCESS
+
+    with mock.patch.object(
+        tuya_cluster.endpoint, "request", side_effect=async_success
+    ) as m1:
+
+        status = await switch_cluster.command(0x0000)
+        m1.assert_called_with(
+            61184,
+            1,
+            b"\x01\x01\x00\x00\x01h\x01\x00\x01\x00",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+        status = await switch_cluster.command(0x0001)
+        m1.assert_called_with(
+            61184,
+            2,
+            b"\x01\x02\x00\x00\x02h\x01\x00\x01\x01",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+        status = switch_cluster.command(0x0003)
+        assert status == foundation.Status.UNSUP_CLUSTER_COMMAND
+
+
+@pytest.mark.parametrize("quirk", (zhaquirks.tuya.valve.SiterwellGS361,))
+async def test_valve_state_report(zigpy_device_from_quirk, quirk):
+    """Test thermostatic valves standard reporting from incoming commands."""
+
+    valve_dev = zigpy_device_from_quirk(quirk)
+    tuya_cluster = valve_dev.endpoints[1].tuya_manufacturer
+
+    thermostat_listener = ClusterListener(valve_dev.endpoints[1].thermostat)
+
+    frames = (
+        ZCL_TUYA_VALVE_TEMPERATURE,
+        ZCL_TUYA_VALVE_TARGET_TEMP,
+        ZCL_TUYA_VALVE_OFF,
+        ZCL_TUYA_VALVE_SCHEDULE,
+        ZCL_TUYA_VALVE_MANUAL,
+    )
+    for frame in frames:
+        hdr, args = tuya_cluster.deserialize(frame)
+        tuya_cluster.handle_message(hdr, args)
+
+    assert len(thermostat_listener.cluster_commands) == 0
+    assert len(thermostat_listener.attribute_updates) == 13
+    assert thermostat_listener.attribute_updates[0][0] == 0x0000  # TEMP
+    assert thermostat_listener.attribute_updates[0][1] == 1790
+    assert thermostat_listener.attribute_updates[1][0] == 0x0012  # TARGET
+    assert thermostat_listener.attribute_updates[1][1] == 500
+    assert thermostat_listener.attribute_updates[2][0] == 0x001C  # OFF
+    assert thermostat_listener.attribute_updates[2][1] == 0x00
+    assert thermostat_listener.attribute_updates[3][0] == 0x001E
+    assert thermostat_listener.attribute_updates[3][1] == 0x00
+    assert thermostat_listener.attribute_updates[4][0] == 0x0029
+    assert thermostat_listener.attribute_updates[4][1] == 0x00
+    assert thermostat_listener.attribute_updates[5][0] == 0x001C  # SCHEDULE
+    assert thermostat_listener.attribute_updates[5][1] == 0x04
+    assert thermostat_listener.attribute_updates[6][0] == 0x0025
+    assert thermostat_listener.attribute_updates[6][1] == 0x01
+    assert thermostat_listener.attribute_updates[7][0] == 0x001E
+    assert thermostat_listener.attribute_updates[7][1] == 0x04
+    assert thermostat_listener.attribute_updates[8][0] == 0x0029
+    assert thermostat_listener.attribute_updates[8][1] == 0x01
+    assert thermostat_listener.attribute_updates[9][0] == 0x001C  # MANUAL
+    assert thermostat_listener.attribute_updates[9][1] == 0x04
+    assert thermostat_listener.attribute_updates[10][0] == 0x0025
+    assert thermostat_listener.attribute_updates[10][1] == 0x00
+    assert thermostat_listener.attribute_updates[11][0] == 0x001E
+    assert thermostat_listener.attribute_updates[11][1] == 0x04
+    assert thermostat_listener.attribute_updates[12][0] == 0x0029
+    assert thermostat_listener.attribute_updates[12][1] == 0x01
+
+
+@pytest.mark.parametrize("quirk", (zhaquirks.tuya.valve.SiterwellGS361,))
+async def test_valve_send_attribute(zigpy_device_from_quirk, quirk):
+    """Test thermostatic valve outgoing commands."""
+
+    valve_dev = zigpy_device_from_quirk(quirk)
+    tuya_cluster = valve_dev.endpoints[1].tuya_manufacturer
+    thermostat_cluster = valve_dev.endpoints[1].thermostat
+
+    async def async_success(*args, **kwargs):
+        return foundation.Status.SUCCESS
+
+    with mock.patch.object(
+        tuya_cluster.endpoint, "request", side_effect=async_success
+    ) as m1:
+
+        status = await thermostat_cluster.write_attributes(
+            {
+                "occupied_heating_setpoint": 2500,
+            }
+        )
+        m1.assert_called_with(
+            61184,
+            1,
+            b"\x01\x01\x00\x00\x01\x02\x02\x00\x04\x00\x00\x00\xfa",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+        status = await thermostat_cluster.write_attributes(
+            {
+                "system_mode": 0x00,
+            }
+        )
+        m1.assert_called_with(
+            61184,
+            2,
+            b"\x01\x02\x00\x00\x02\x04\x04\x00\x01\x00",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+        status = await thermostat_cluster.write_attributes(
+            {
+                "system_mode": 0x04,
+            }
+        )
+        m1.assert_called_with(
+            61184,
+            3,
+            b"\x01\x03\x00\x00\x03\x04\x04\x00\x01\x02",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+        status = await thermostat_cluster.write_attributes(
+            {
+                "programing_oper_mode": 0x01,
+            }
+        )
+        m1.assert_called_with(
+            61184,
+            4,
+            b"\x01\x04\x00\x00\x04\x04\x04\x00\x01\x01",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+        # simulate a target temp update so that relative changes can work
+        hdr, args = tuya_cluster.deserialize(ZCL_TUYA_VALVE_TARGET_TEMP)
+        tuya_cluster.handle_message(hdr, args)
+        status = await thermostat_cluster.command(0x0000, 0x00, 20)
+        m1.assert_called_with(
+            61184,
+            5,
+            b"\x01\x05\x00\x00\x05\x02\x02\x00\x04\x00\x00\x00F",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+        status = await thermostat_cluster.command(0x0002)
+        assert status == foundation.Status.UNSUP_CLUSTER_COMMAND
+
+
+@pytest.mark.parametrize("quirk", (zhaquirks.tuya.electric_heating.MoesBHT,))
+async def test_eheating_state_report(zigpy_device_from_quirk, quirk):
+    """Test thermostatic valves standard reporting from incoming commands."""
+
+    electric_dev = zigpy_device_from_quirk(quirk)
+    tuya_cluster = electric_dev.endpoints[1].tuya_manufacturer
+
+    thermostat_listener = ClusterListener(electric_dev.endpoints[1].thermostat)
+
+    frames = (ZCL_TUYA_EHEAT_TEMPERATURE, ZCL_TUYA_EHEAT_TARGET_TEMP)
+    for frame in frames:
+        hdr, args = tuya_cluster.deserialize(frame)
+        tuya_cluster.handle_message(hdr, args)
+
+    assert len(thermostat_listener.cluster_commands) == 0
+    assert len(thermostat_listener.attribute_updates) == 2
+    assert thermostat_listener.attribute_updates[0][0] == 0x0000  # TEMP
+    assert thermostat_listener.attribute_updates[0][1] == 1790
+    assert thermostat_listener.attribute_updates[1][0] == 0x0012  # TARGET
+    assert thermostat_listener.attribute_updates[1][1] == 2100
+
+
+@pytest.mark.parametrize("quirk", (zhaquirks.tuya.electric_heating.MoesBHT,))
+async def test_eheat_send_attribute(zigpy_device_from_quirk, quirk):
+    """Test electric thermostat outgoing commands."""
+
+    eheat_dev = zigpy_device_from_quirk(quirk)
+    tuya_cluster = eheat_dev.endpoints[1].tuya_manufacturer
+    thermostat_cluster = eheat_dev.endpoints[1].thermostat
+
+    async def async_success(*args, **kwargs):
+        return foundation.Status.SUCCESS
+
+    with mock.patch.object(
+        tuya_cluster.endpoint, "request", side_effect=async_success
+    ) as m1:
+
+        status = await thermostat_cluster.write_attributes(
+            {
+                "occupied_heating_setpoint": 2500,
+            }
+        )
+        m1.assert_called_with(
+            61184,
+            1,
+            b"\x01\x01\x00\x00\x01\x10\x02\x00\x04\x00\x00\x00\x19",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+        status = await thermostat_cluster.write_attributes(
+            {
+                "system_mode": 0x00,
+            }
+        )
+        m1.assert_called_with(
+            61184,
+            2,
+            b"\x01\x02\x00\x00\x02\x01\x01\x00\x01\x00",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+        status = await thermostat_cluster.write_attributes(
+            {
+                "system_mode": 0x04,
+            }
+        )
+        m1.assert_called_with(
+            61184,
+            3,
+            b"\x01\x03\x00\x00\x03\x01\x01\x00\x01\x01",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+        # simulate a target temp update so that relative changes can work
+        hdr, args = tuya_cluster.deserialize(ZCL_TUYA_EHEAT_TARGET_TEMP)
+        tuya_cluster.handle_message(hdr, args)
+        status = await thermostat_cluster.command(0x0000, 0x00, 20)
+        m1.assert_called_with(
+            61184,
+            4,
+            b"\x01\x04\x00\x00\x04\x10\x02\x00\x04\x00\x00\x00\x17",
+            expect_reply=False,
+            command_id=0,
+        )
+        assert status == (foundation.Status.SUCCESS,)
+
+        status = await thermostat_cluster.command(0x0002)
+        assert status == foundation.Status.UNSUP_CLUSTER_COMMAND