For convenience, I’ll consolidate all the information into one thread. Maybe this will make it easier for someone more knowledgeable about this quirk to work on it.
So, what I have managed to find out so far:
- List of data points for the device :
"2": "Mode",
"3": "Working status",
"4": "Set Temp.",
"5": "Current Temp.",
"6": "Battery capacity",
"7": " lock",
"14": "Window opening detection",
"15": "State of the window",
"21": "Holiday Temp.",
"28": "Week Program",
"29": "Tuesday",
"30": "Wednesday",
"31": "Thursday",
"32": "Friday",
"33": "Saturday",
"34": "Sunday",
"35": "Fault alarm",
"36": "Frost protection",
"47": "Temp. Calibration",
"101": "Switch",
"102": "Temp. Accuracy",
"103": "Eco Temp. Set",
"104": "Comfort Temp. Set",
"105": "Antifrost Temp. Set"
Device signature in ZHA:
{
"node_descriptor": {
"logical_type": 2,
"complex_descriptor_available": 0,
"user_descriptor_available": 0,
"reserved": 0,
"aps_flags": 0,
"frequency_band": 8,
"mac_capability_flags": 128,
"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": "0x0301",
"input_clusters": [
"0x0000",
"0x0001",
"0x0004",
"0x0005",
"0x0201",
"0x0204",
"0xef00"
],
"output_clusters": [
"0x000a",
"0x0019"
]
},
"2": {
"profile_id": "0x0104",
"device_type": "0x0007",
"input_clusters": [
"0xfd00"
],
"output_clusters": []
}
},
"manufacturer": "_TZE204_eekpf0ft",
"model": "TS0601",
"class": "ts0601_trv_m3z.TuyaTRV"
}
I’m still figuring out how to create a quirk for ZHA. So far, I’ve managed to get the device’s basic functions working. At this stage, I can turn the heating on/off and select the temperature. The battery status display also works correctly. I haven’t tested the threshold settings yet.
Also, here’s a quirk code based on this: [Github]: zha-device-handlers/zhaquirks/tuya/ts0601_trv_tze204.py at 94e15bdcdeeab9dc333fbd587416bffd1b76048f · Teka101/zha-device-handlers · GitHub
I think we should build on this by adding/changing missing/incorrect clusters. I just need to know how to do it correctly. My programming skills are still too weak for that 
Here is the code I use:
from typing import Optional, Union
from zigpy.profiles import zha
import zigpy.types as t
from zigpy.zcl import foundation
from zigpy.zcl.clusters.general import (
AnalogOutput,
Basic,
Groups,
OnOff,
Ota,
Scenes,
Time,
)
from zhaquirks import Bus, LocalDataCluster
from zhaquirks.const import (
DEVICE_TYPE,
ENDPOINTS,
INPUT_CLUSTERS,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
)
from zhaquirks.tuya import (
TUYA_DP_TYPE_BOOL,
TUYA_DP_TYPE_ENUM,
TUYA_DP_TYPE_FAULT,
TUYA_DP_TYPE_VALUE,
TuyaManufClusterAttributes,
TuyaPowerConfigurationCluster2AA,
TuyaThermostat,
TuyaThermostatCluster,
TuyaUserInterfaceCluster,
)
# Data Points based on your device configuration
TRV_PRESET = TUYA_DP_TYPE_ENUM + 2
TRV_TARGET_TEMP = TUYA_DP_TYPE_VALUE + 4
TRV_LOCAL_TEMP = TUYA_DP_TYPE_VALUE + 5
TRV_BATTERY = TUYA_DP_TYPE_VALUE + 6
TRV_CHILD_LOCK = TUYA_DP_TYPE_BOOL + 7
TRV_SYSTEM_MODE = TUYA_DP_TYPE_BOOL + 101
TuyaManufClusterSelf = None
class ManufCluster(TuyaManufClusterAttributes):
"""Manufacturer Specific Cluster for Tuya TRV."""
def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
global TuyaManufClusterSelf
TuyaManufClusterSelf = self
# FIX: Set the time offset to avoid AssertionError
self.set_time_local_offset = 1970
set_time_offset = 1970
attributes = TuyaManufClusterAttributes.attributes.copy()
attributes.update(
{
TRV_PRESET: ("preset", t.uint8_t, True),
TRV_TARGET_TEMP: ("target_temp", t.uint32_t, True),
TRV_LOCAL_TEMP: ("local_temp", t.uint32_t, True),
TRV_BATTERY: ("battery", t.uint32_t, True),
TRV_CHILD_LOCK: ("child_lock", t.uint8_t, True),
TRV_SYSTEM_MODE: ("system_mode", t.uint8_t, True),
}
)
def _update_attribute(self, attrid, value):
super()._update_attribute(attrid, value)
# Temperature handling
if attrid in (TRV_TARGET_TEMP, TRV_LOCAL_TEMP):
# Convert decidegree to centidegree (divide by 10)
self.endpoint.device.thermostat_bus.listener_event(
"temperature_change",
attrid,
value * 10,
)
elif attrid == TRV_BATTERY:
self.endpoint.device.battery_bus.listener_event("battery_change", value)
elif attrid == TRV_PRESET:
self.endpoint.device.preset_bus.listener_event("preset_change", value)
class ThermostatCluster(TuyaThermostatCluster):
"""Thermostat cluster for Tuya TRV."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set temperature limits
self.endpoint.device.thermostat_bus.listener_event(
"temperature_change", "min_heat_setpoint_limit", 500
)
self.endpoint.device.thermostat_bus.listener_event(
"temperature_change", "max_heat_setpoint_limit", 3500
)
def map_attribute(self, attribute, value):
"""Map standard attribute to Tuya DP."""
if attribute == "occupied_heating_setpoint":
return {TRV_TARGET_TEMP: round(value / 10)} # centidegree to decidegree
if attribute == "local_temperature":
return {TRV_LOCAL_TEMP: round(value / 10)} # centidegree to decidegree
if attribute == "system_mode":
if value == self.SystemMode.Off:
return {TRV_SYSTEM_MODE: 0}
if value == self.SystemMode.Heat:
return {TRV_SYSTEM_MODE: 1}
return None
class PresetSelect(LocalDataCluster):
"""Custom cluster for preset selection."""
cluster_id = 0xFD00 # Custom cluster ID
class Preset(t.enum8):
Manual = 0x00
Schedule = 0x01
Eco = 0x02
Comfort = 0x03
FrostProtect = 0x04
Holiday = 0x05
Off = 0x06
attributes = {
0x0000: ("preset", Preset, True),
}
def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
self.endpoint.device.preset_bus.add_listener(self)
def preset_change(self, value):
"""Update preset from device."""
if 0 <= value <= 6:
self._update_attribute(0x0000, value)
async def write_attributes(self, attributes, manufacturer=None):
"""Write preset to device."""
for attrid, value in attributes.items():
if attrid == 0x0000: # preset
await TuyaManufClusterSelf.endpoint.tuya_manufacturer.write_attributes(
{TRV_PRESET: value}, manufacturer=None
)
return [foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]
return [foundation.WriteAttributesStatusRecord(foundation.Status.FAILURE)]
class UserInterfaceCluster(TuyaUserInterfaceCluster):
"""User interface cluster for child lock."""
_CHILD_LOCK_ATTR = TRV_CHILD_LOCK
class TuyaTRV(TuyaThermostat):
"""Tuya TRV device."""
def __init__(self, *args, **kwargs):
"""Init device."""
self.preset_bus = Bus()
super().__init__(*args, **kwargs)
signature = {
MODELS_INFO: [
("_TZE204_eekpf0ft", "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,
ManufCluster,
ThermostatCluster,
UserInterfaceCluster,
TuyaPowerConfigurationCluster2AA,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
2: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE,
INPUT_CLUSTERS: [
PresetSelect,
],
OUTPUT_CLUSTERS: [],
}
}
}
I’ll continue trying to figure this out on my own, but I’d be grateful for any help
Sorry for my bad English