zigbee2mqtt icon indicating copy to clipboard operation
zigbee2mqtt copied to clipboard

[New device support]: TS0601 TRV Devices - Unsupported

Open Forstfb1 opened this issue 1 year ago • 8 comments

Link

https://www.aliexpress.com/item/1005005454936849.html?spm=a2g0o.order_list.order_list_main.5.5f321802cPC0l2

Database entry

{"id":88,"type":"EndDevice","ieeeAddr":"0xa4c1389273b1279f","nwkAddr":14059,"manufId":4417,"manufName":"_TZE204_zljqtner","powerSource":"Battery","modelId":"TS0601","epList":[1],"endpoints":{"1":{"profId":260,"epId":1,"devId":81,"inClusterList":[0,4,5,61184],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"65487":14400,"65503":"G�.iH�.iI�.iK�.iN�.iR�.iU�.fV�.\u0012","65506":56,"65508":0,"stackVersion":0,"dateCode":"","appVersion":74}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":74,"stackVersion":0,"hwVersion":1,"dateCode":"","zclVersion":3,"interviewCompleted":true,"meta":{},"lastSeen":1724761166920} {"id":89,"type":"EndDevice","ieeeAddr":"0xa4c138425dcc44d1","nwkAddr":41530,"manufId":4417,"manufName":"_TZE204_zljqtner","powerSource":"Battery","modelId":"TS0601","epList":[1],"endpoints":{"1":{"profId":260,"epId":1,"devId":81,"inClusterList":[0,4,5,61184],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"65487":14400,"65503":"\u0000\u0000\u0000\u0000\u0011\u0000\u0000\u0000\u0000\u0011","65506":56,"65508":1,"modelId":"TS0601","manufacturerName":"_TZE204_zljqtner","powerSource":3,"zclVersion":3,"appVersion":74,"stackVersion":0,"hwVersion":1,"dateCode":""}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":74,"stackVersion":0,"hwVersion":1,"dateCode":"","zclVersion":3,"interviewCompleted":true,"meta":{},"lastSeen":1724761172904}

Comments

image I'm a complete newbie, and love the way that 99.9% of the items i have added "Just Work" this is the first time i have come across an item that did... please help :

External definition

const definition = {
    zigbeeModel: ['TS0601'],
    model: 'TS0601',
    vendor: '_TZE204_zljqtner',
    description: 'Automatically generated definition',
    extend: [],
    meta: {},
};

module.exports = definition;

Forstfb1 avatar Aug 27 '24 12:08 Forstfb1

Any update and advise yet how to integrate this device properly ?

atzetot avatar Sep 23 '24 13:09 atzetot

I have exact same issue - valve is not recognized

banolka avatar Sep 27 '24 14:09 banolka

У меня такая же проблема. Решение кто нибудь подскажет?

lenivez123 avatar Oct 12 '24 17:10 lenivez123

https://houseiq.pl/pl/p/Glowica-termostatyczna-GTZ06-ZigBee-3.0-TUYA/1620

I bought my thermostatic radiator valve at the end of September. I couldn't add it correctly - no support. is there any chance to add support for this thermostatic head?

const definition = { zigbeeModel: ['TS0601'], model: 'TS0601', vendor: '_TZE204_zljqtner', description: 'Automatically generated definition', extend: [], meta: {}, };

I also failed to add it using custom quirks

module.exports = definition; Zrzut ekranu z 2024-11-01 11-48-31

rdkm82 avatar Nov 04 '24 15:11 rdkm82

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days

github-actions[bot] avatar Jan 04 '25 00:01 github-actions[bot]

I managed to get SOME functionality using z2m-edge and converters file. I can get/set temperature, offset, window, child lock. I get idle/heating state back, not sure if battery works correctly. I have not tested the schedules.

Here is the converters.js content:

const fz = require('zigbee-herdsman-converters/converters/fromZigbee'); const tz = require('zigbee-herdsman-converters/converters/toZigbee'); const reporting = require('zigbee-herdsman-converters/lib/reporting'); const modernExtend = require('zigbee-herdsman-converters/lib/modernExtend'); const exposes = require('zigbee-herdsman-converters/lib/exposes'); const tuya = require('zigbee-herdsman-converters/lib/tuya'); const utils = require('zigbee-herdsman-converters/lib/utils'); const e = exposes.presets; const ea = exposes.access;

const definition = { fingerprint: [ {modelID: 'TS0601', manufacturerName: '_TZE204_zljqtner'}, ], model: 'TS0601_thermostat_1', vendor: 'TuYa', description: 'Thermostatic radiator valve', whiteLabel: [ {vendor: 'Unknown/id3.pl', model: 'GTZ06'}, ], onEvent: tuya.onEventSetLocalTime, fromZigbee: [tuya.fz.datapoints], toZigbee: [tuya.tz.datapoints], configure: tuya.configureMagicPacket, exposes: [ e.battery(), e.child_lock(), e.max_temperature().withValueMin(15).withValueMax(45), e.min_temperature().withValueMin(5).withValueMax(15), e.window_detection(), e.binary('window', ea.STATE, 'CLOSED', 'OPEN').withDescription('Window status closed or open '), e.binary('alarm_switch', ea.STATE, 'ON', 'OFF').withDescription('Thermostat in error state'), e.climate() .withLocalTemperature(ea.STATE) .withSetpoint('current_heating_setpoint', 5, 35, 0.5, ea.STATE_SET) .withLocalTemperatureCalibration(-30, 30, 0.1, ea.STATE_SET) .withPreset(['auto', 'manual', 'off', 'on'], 'MANUAL MODE ☝ - In this mode, the device executes manual temperature setting. ' + 'When the set temperature is lower than the "minimum temperature", the valve is closed (forced closed). ' + 'AUTO MODE ⏱ - In this mode, the device executes a preset week programming temperature time and temperature. ' + 'ON - In this mode, the thermostat stays open ' + 'OFF - In this mode, the thermostat stays closed') .withSystemMode(['auto', 'heat', 'off'], ea.STATE) .withRunningState(['idle', 'heat'], ea.STATE), ...tuya.exposes.scheduleAllDays(ea.STATE_SET, 'HH:MM/C HH:MM/C HH:MM/C HH:MM/C'), ], meta: { tuyaDatapoints: [ [1, null, { from: (v) => { utils.assertNumber(v, 'system_mode'); const presetLookup = {0: 'auto', 1: 'manual', 2: 'off', 3: 'on'}; const systemModeLookup = {0: 'auto', 1: 'auto', 2: 'off', 3: 'heat'}; return {preset: presetLookup[v], system_mode: systemModeLookup[v]}; }, }, ], [1, 'system_mode', tuya.valueConverterBasic.lookup({'auto': tuya.enum(1), 'off': tuya.enum(2), 'heat': tuya.enum(3)})], [1, 'preset', tuya.valueConverterBasic.lookup( {'auto': tuya.enum(0), 'manual': tuya.enum(1), 'off': tuya.enum(2), 'on': tuya.enum(3)})], [2, 'current_heating_setpoint', tuya.valueConverter.divideBy10], [3, 'local_temperature', tuya.valueConverter.divideBy10], [6, 'running_state', tuya.valueConverterBasic.lookup({'heat': 1, 'idle': 0})], [7, 'window', tuya.valueConverterBasic.lookup({'OPEN': 1, 'CLOSE': 0})], [8, 'window_detection', tuya.valueConverter.onOff], [9, 'max_temperature', tuya.valueConverter.divideBy10], [10, 'min_temperature', tuya.valueConverter.divideBy10], [12, 'child_lock', tuya.valueConverter.lockUnlock], [13, 'battery', tuya.valueConverter.raw], //[14, 'alarm_switch', tuya.valueConverter.onOff], [101, 'local_temperature_calibration', tuya.valueConverter.localTempCalibration1], [102, 'schedule_monday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(1)], [103, 'schedule_tuesday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(2)], [104, 'schedule_wednesday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(3)], [105, 'schedule_thursday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(4)], [106, 'schedule_friday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(5)], [107, 'schedule_saturday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(6)], [108, 'schedule_sunday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(7)], ], }, };

module.exports = definition;

makobpl avatar Jan 06 '25 20:01 makobpl

Hi there, Here are the codes I obtained for the tuyadatapoints. Not sure how to implement that properly in an external converter, if someone could help, that'd be appreciated. best,

{ "1":"Mode", "2":"Target temperature", "3":"Current temperature", "6":"Working status", "7":"Window status", "8":"Open window", "9":"Max. limit temperature", "10":"Min. limit temperature", "12":"Child lock", "13":"Battery", "14":"Fault alarm", "101":"Room sensor calibration", "102":"周程序1", // Weekly program 1 "103":"周程序2", // Weekly program 2 "104":"周程序3", // Weekly program 3 "105":"周程序4", // Weekly program 4 "106":"周程序5", // Weekly program 5 "107":"周程序6", // Weekly program 6 "108":"周程序7", // Weekly program 7 "109":"机型", //model "110":"Motor thrust", "111":"Display brightness", "112":"Software version", "113":"Screen orientation", "114":"Valve", "115":"Switch deviation (energy-saving mode only)", "116":"电机数据", //motor data "117":"假期时长", //Holiday duration "118":"Boost 模式时长", //Boost mode duration "119":"舒适模式温度", //Comfort mode temperature "120":"节能模式温度", //Energy saving mode temperature "121":"防冻模式温度", //Antifreeze mode temperature "122":"防冻使能", //Antifreeze enable "123":"气压指数", //pressure index "124":"临时模式开始时间", //Temporary mode start time "125":"功能点", //Function point "126":"app支持特性", //app support features "127":"System mode" }

vjaunet avatar Jan 16 '25 13:01 vjaunet

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days

github-actions[bot] avatar Mar 18 '25 00:03 github-actions[bot]

Hi there, I have been working on tuning down the external converter for this device, and I'm missing just one thing: Synchronizing Time. That is, if I link the device to SmartLife, the schedule works as expected, then I switch it to zigbee2mqtt stack, it still works, but I observe that it looses 8sec every hour, so the next day, it's completely out of sync.

In z2m logs I see:

[2025-12-14 20:02:34] debug: zh:controller: Received payload: clusterID=61184, address=60516, groupID=0, endpoint=1, destinationEndpoint=1, wasBroadcast=false, linkQuality=78, frame={"header":{"frameControl":{"frameType":1,"manufacturerSpecific":false,"direction":1,"disableDefaultResponse":false,"reservedBits":0},"transactionSequenceNumber":156,"commandIdentifier":36},"payload":{"payloadSize":160},"command":{"ID":36,"parameters":[{"name":"payloadSize","type":33}],"name":"mcuSyncTime"}} [2025-12-14 20:02:34] debug: zh:controller:endpoint: ZCL command 0xa4c1386fbd1a0125/1 manuSpecificTuya.defaultRsp({"cmdId":36,"statusCode":0}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"transactionSequenceNumber":156,"writeUndiv":false}) [2025-12-14 20:02:34] debug: zh:zstack: sendZclFrameToEndpointInternal 0xa4c1386fbd1a0125:60516/1 (0,0,1) [2025-12-14 20:02:34] debug: zh:zstack:znp: --> SREQ: AF - dataRequest - {"dstaddr":60516,"destendpoint":1,"srcendpoint":1,"clusterid":61184,"transid":4,"options":0,"radius":30,"len":5,"data":{"type":"Buffer","data":[16,156,11,36,0]}} [2025-12-14 20:02:34] debug: zh:zstack:unpi:writer: --> frame [254,15,36,1,100,236,1,1,0,239,4,0,30,5,16,156,11,36,0,241] [2025-12-14 20:02:34] debug: zh:zstack:unpi:parser: --- parseNext [] [2025-12-14 20:02:34] debug: z2m: Received Zigbee message from 'TRV-SdB', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":160}' from endpoint 1 with groupID 0 [2025-12-14 20:02:34] debug: z2m: No converter available for 'TRV601' with cluster 'manuSpecificTuya' and type 'commandMcuSyncTime' and data '{"payloadSize":160}'

I do have the following line, but I still see the no converter available [...] "commandMcuSyncTime":

onEvent: tuya.onEventSetTime,

I even tried to make a custom function, but it's as if the onEvent was never fired. I'm unsure if this is a leftover from older versions of z2m, and unsure how to do it now, with the following versions:

  • Frontend version 2.4.2
  • zigbee-herdsman-converters version 25.80.0
  • zigbee-herdsman version 7.0.1
  • Zigbee2MQTT version 2.7.0

Levak avatar Dec 14 '25 19:12 Levak

The fix in this post seems to work and resync the time properly for this device too. I removed the onEvent attribute. I re-paired, and the schedule was back on track.

Here is the updated external converter:

const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const modernExtend = require('zigbee-herdsman-converters/lib/modernExtend');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const tuya = require('zigbee-herdsman-converters/lib/tuya');
const utils = require('zigbee-herdsman-converters/lib/utils');
const legacy = require('zigbee-herdsman-converters/lib/legacy');
const logger = require('zigbee-herdsman-converters/lib/logger');

const e = exposes.presets;
const ea = exposes.access;

const allModes = {
    0: { preset:"off"        , systemMode: "off",  deviceMode: "off"        , isSystemMode: true },
    1: { preset:"comfort"    , systemMode: "auto", deviceMode: "comfort"    , },
    2: { preset:"manual"     , systemMode: "auto", deviceMode: "manual"     , },
    3: { preset:"on"         , systemMode: "heat", deviceMode: "on"         , isSystemMode: true },
    4: { preset:"eco"        , systemMode: "auto", deviceMode: "eco"        , },
    5: { preset:"anti-frost" , systemMode: "off",  deviceMode: "anti-frost" , },
    6: { preset:"schedule"   , systemMode: "auto", deviceMode: "schedule"   , isSystemMode: true },
};
const allModesByName = Object.fromEntries(
    Object.entries(allModes)
        .map(([k, v]) => [
            v.preset, { ...v, 'index': k }
        ])
);

function customThermostatScheduleDay() {
    /// Each day is split into slots of 30minutes.
    /// Each slot is 4 bits, encoding the mode in which the valve should be put into.
    /// e.g. 69 20 ...
    /// Means at 00:00 use mode (69 >> 4) = 4 aka "eco"
    ///       at 00:30 use mode (69 & 15) = 5 aka "anti-frost"
    ///       at 01:00 use mode (20 >> 4) = 1 aka "comfort"
    ///       at 01:30 use mode (20 & 15) = 4 aka "eco"
    return {
        from: (v) => {
            const daySchedule = [];
            let previousMode = null;
            for (let index = 0; index < 48; index++) {
                const hour = Math.floor(index / 2);
                const minutes = (index % 2) ? 30 : 0;
                const hour_values = parseInt(v[hour], 10);
                const mode = (index % 2) ? (hour_values & 15) : (hour_values >> 4);
                if (allModes[mode] === undefined) {
                    throw new Error(`Invalid mode half-byte: "${mode}" (${hour_values} index ${index})`);
                }
                const modeString = allModes[mode].preset;
                if (mode != previousMode)
                {
                    const schedule = String(hour).padStart(2, '0') +
                          ':' +
                          String(minutes).padStart(2, '0') +
                          '/' +
                          modeString;
                    daySchedule.push(schedule);
                    previousMode = mode;
                }
            }
            return Array.from(new Set(daySchedule)).join(' ');
        },
        to: (v) => {
            const parseTransition = (transition) => {
                const parts = transition.split('/');
                if (parts.length !== 2) {
                    throw new Error(`Invalid schedule: wrong transition format "${transition}"`);
                }
                const [timePart, setting] = parts;
                const timeParts = timePart.split(':');
                if (timeParts.length !== 2) {
                    throw new Error(`Invalid time format in: "${transition}"`);
                }
                const hour = parseInt(timeParts[0], 10);
                const min = parseInt(timeParts[1], 10);
                if (hour < 0 || hour > 23 || (min != 0 && min != 30)) {
                    throw new Error(`Invalid hour or minute in: "${transition}"`);
                }
                return { minutesSinceMidnight: hour * 60 + min, setting };
            };

            const inputTransitions = v.split(/\s+/).filter(Boolean);
            if (inputTransitions.length < 1 || inputTransitions.length > 48) {
                throw new Error(`Invalid schedule: there should be between 1 and 48 transitions, got "${inputTransitions.length}"`);
            }

            const transitions = [];
            const seenTimes = new Set();
            for (const inputTransition of inputTransitions) {
                const transition = parseTransition(inputTransition);
                if (!seenTimes.has(transition.minutesSinceMidnight)) {
                    seenTimes.add(transition.minutesSinceMidnight);
                    transitions.push(transition);
                }
            }

            if (transitions.length === 0) {
                throw new Error('No valid transitions found.');
            }

            transitions.sort((a, b) => a.minutesSinceMidnight - b.minutesSinceMidnight);

            const payload = [];

            function encodeNextMode(minutesSinceMidnight, setting) {
                // Device only supports these 3 modes in schedule mode
                if (setting != "eco" && setting != "comfort") {
                    setting = "anti-frost";
                }
                const mode = allModesByName[setting.toLowerCase()];
                if (!mode) {
                    throw new Error(`Invalid mode: "${setting}"`);
                }
                if ((minutesSinceMidnight % 60) == 0) {
                    payload.push(mode.index << 4);
                } else {
                    payload[payload.length - 1] |= (mode.index & 15);
                }
            }

            let previousTime = 0;
            let previousMode = null;

            for (const { minutesSinceMidnight, setting } of transitions) {
                if (previousMode == null) {
                    previousMode = setting;
                }
                for (let time = previousTime + 30; time < minutesSinceMidnight; time += 30) {
                    encodeNextMode(time, previousMode);
                }
                encodeNextMode(minutesSinceMidnight, setting);
                previousTime = minutesSinceMidnight;
                previousMode = setting;
            }

            if (previousMode != null) {
                for (let time = previousTime + 30; time < 24*60; time += 30) {
                    encodeNextMode(time, previousMode);
                }
            }

            return payload;
        }
    }
}

const tuya_set_time_request = {
    cluster: 'manuSpecificTuya',
    type: ['commandMcuSyncTime'],
    convert: async (model, msg, publish, options, meta) => {
        const OneJanuary2000 = new Date('January 01, 2000 00:00:00 UTC+00:00').getTime();
        const currentTime = new Date().getTime();
        const utcTime = Math.round((currentTime - OneJanuary2000) / 1000);
        const localTime = Math.round(currentTime / 1000) - (new Date()).getTimezoneOffset() * 60;
        const endpoint = msg.endpoint;
        const payload = {
            payloadSize: 8,
            payload: [
                ...tuya.convertDecimalValueTo4ByteHexArray(utcTime),
                ...tuya.convertDecimalValueTo4ByteHexArray(localTime),
            ],
        };
        await endpoint.command('manuSpecificTuya', 'mcuSyncTime', payload, {});
    },
};

const definition = {
    fingerprint: tuya.fingerprint("TS0601", ["_TZE204_zljqtner"]),
    model: "TRV601Z",
    vendor: "TuYa", // White label
    description: "Thermostatic radiator valve",
    fromZigbee: [tuya.fz.datapoints, tuya_set_time_request],
    toZigbee: [tuya.tz.datapoints],
    configure: tuya.configureMagicPacket,
    exposes: [
        e
            .battery()
            .withCategory('diagnostic'),
        e
            .binary("window", ea.STATE, "CLOSED", "OPEN")
            .withCategory('diagnostic')
            .withDescription("Window status: closed or open."),
        e
            .enum("valve", ea.STATE, ["CLOSED", "OPEN"])
            .withCategory('diagnostic')
            .withDescription("Valve status: closed or open."),
        e
            .numeric("holiday_duration", ea.STATE_SET)
            .withUnit("day")
            .withValueMin(0)
            .withValueMax(60)
            .withValueStep(1)
            .withDescription("Duration of the holiday mode before going back to previous mode. If the value is other than 0, then holiday mode is active."),
        e
            .numeric("boost_duration", ea.STATE_SET)
            .withUnit("min")
            .withValueMin(0)
            .withValueMax(120)
            .withValueStep(1)
            .withDescription("Duration of the boost mode before going back to previous mode. If the value is other than 0, then boost mode is active."),
        e
            .climate()
            .withLocalTemperature(ea.STATE)
            .withSetpoint("current_heating_setpoint", 5, 35, 0.5, ea.STATE_SET)
            .withLocalTemperatureCalibration(-10, 10, 0.1, ea.STATE_SET)
            .withPreset(Object.values(allModes).map(e => e.preset),
                "// Comfort -- the device setpoint is set to comfort temperature.  " +
                "// Eco -- the device setpoint is set to eco temperature.  " +
                "// Manual --  the device setpoint can be adjusted manually. " +
                "When the set temperature is lower than the \"temperature set point\", the valve is closed (forced closed). " +
                "// ON -- In this mode, the thermostat stays open. " +
                "// OFF -- In this mode, the thermostat stays closed.  " +
                "// Anti-frost -- device setpoint is set to anti-frost temperature.  " +
                "// Schedule -- device setpoint is changed according to the scheduled parameters."
            )
            .withSystemMode(["auto", "heat", "off"], ea.STATE_SET)
            .withRunningState(["idle", "heat"], ea.STATE),
        e
            .child_lock(),
        e
            .comfort_temperature()
            .withCategory('config')
            .withDescription("Comfort mode temperature")
            .withValueMin(18)
            .withValueMax(35)
            .withValueStep(0.5),
        e
            .eco_temperature()
            .withCategory('config')
            .withDescription("Eco mode temperature")
            .withValueMin(5)
            .withValueMax(20)
            .withValueStep(0.5),
        ...tuya.exposes.scheduleAllDays(ea.STATE_SET, "00:00/eco 00:30/comfort 01:00/anti-frost ...")
            .map(e => e.withCategory('config')),
        e
            .binary("frost_protection", ea.STATE_SET, "ON", "OFF")
            .withCategory('config')
            .withDescription(
                "When the room temperature is lower than 5 °C (or the choosen one), the valve opens; when the temperature rises to 8 °C, the valve closes",
            ),
        e
            .numeric("frost_protection_temperature", ea.STATE_SET)
            .withUnit("°C")
            .withCategory('config')
            .withValueMin(5)
            .withValueMax(18)
            .withValueStep(0.5)
            .withDescription(""),
        e
            .numeric("switch_deviation", ea.STATE_SET)
            .withUnit("°C")
            .withCategory('config')
            .withValueMin("0.5")
            .withValueMax("5")
            .withValueStep("0.1")
            .withDescription(
                "Hysteresis - comfort > switches off/on exactly at reached temperature with valve smooth from 0 to 100%, eco > 0.5 degrees above or below, valve either 0 or 100%",
            ),
        e
            .max_temperature()
            .withCategory('config')
            .withValueMin(20)
            .withValueMax(35)
            .withValueStep(1),
        e
            .min_temperature()
            .withCategory('config')
            .withValueMin(5)
            .withValueMax(15)
            .withValueStep(1),
        e
            .enum("brightness", ea.STATE_SET, ["low", "medium", "high"])
            .withCategory('config')
            .withDescription("Screen Brightness"),
        e
            .enum("screen_orientation", ea.STATE_SET, ["up", "down"])
            .withCategory('config')
            .withDescription("Screen orientation"),
        e
            .enum("motor_thrust", ea.STATE_SET, ["strong", "middle", "weak"])
            .withCategory('config'),
        e
            .binary("alarm_switch", ea.STATE, "ON", "OFF")
            .withCategory('diagnostic')
            .withDescription("Thermostat in error state"),
        e
            .binary('window_detection', ea.STATE_SET, 'ON', 'OFF')
            .withDescription('Enables/disables window detection on the device')
            .withCategory('config'),
    ],
    meta: {
        tuyaDatapoints: [
            [
                1,
                null,
                tuya.valueConverter.thermostatSystemModeAndPresetMap({
                    fromMap: allModes,
                }),
            ],
            [
                1,
                "system_mode",
                tuya.valueConverter.thermostatSystemModeAndPresetMap({
                    toMap: Object.fromEntries(
                        Object.entries(allModes).filter(e => e[1].isSystemMode)
                            .map(([k, v]) => [
                                v.systemMode, new tuya.Enum(k)
                            ])
                    ),
                }),
            ],
            [
                1,
                "preset",
                tuya.valueConverter.thermostatSystemModeAndPresetMap({
                    toMap: Object.fromEntries(
                        Object.entries(allModes)
                            .map(([k, v]) => [
                                v.preset, tuya.enum(parseInt(k))
                            ])
                    ),
                }),
            ],

            [2, "current_heating_setpoint", tuya.valueConverter.divideBy10],
            [3, "local_temperature", tuya.valueConverter.divideBy10],
            [6, "running_state", tuya.valueConverterBasic.lookup({heat: 1, idle: 0})],
            [7, "window", tuya.valueConverterBasic.lookup({OPEN: 1, CLOSE: 0})],
            [8, "window_detection", tuya.valueConverter.onOff],
            [9, "max_temperature", tuya.valueConverter.divideBy10],
            [10, "min_temperature", tuya.valueConverter.divideBy10],
            [12, "child_lock", tuya.valueConverter.lockUnlock],
            [13, "battery", tuya.valueConverter.raw],
            [14, "alarm_switch", tuya.valueConverter.onOff],
            [101, "local_temperature_calibration", tuya.valueConverter.localTempCalibration1],
            [102, "schedule_sunday", customThermostatScheduleDay()],
            [103, "schedule_monday", customThermostatScheduleDay()],
            [104, "schedule_tuesday", customThermostatScheduleDay()],
            [105, "schedule_wednesday", customThermostatScheduleDay()],
            [106, "schedule_thursday", customThermostatScheduleDay()],
            [107, "schedule_friday", customThermostatScheduleDay()],
            [108, "schedule_saturday", customThermostatScheduleDay()],
            [
                110,
                "motor_thrust",
                tuya.valueConverterBasic.lookup({
                    strong: tuya.enum(0),
                    middle: tuya.enum(1),
                    weak: tuya.enum(2),
                }),
            ],
            [
                111,
                "brightness",
                tuya.valueConverterBasic.lookup({
                    low: tuya.enum(2),
                    medium: tuya.enum(1),
                    high: tuya.enum(0),
                }),
            ],
            [
                113,
                "screen_orientation",
                tuya.valueConverterBasic.lookup({
                    up: tuya.enum(0),
                    down: tuya.enum(1),
                }),
            ],
            [114, "valve", tuya.valueConverterBasic.lookup({ OPEN: 1000, CLOSED: 0 })],
            [115, "switch_deviation", tuya.valueConverter.divideBy10],
            [117, "holiday_duration", tuya.valueConverter.raw],
            [118, "boost_duration", tuya.valueConverter.raw],
            [119, "comfort_temperature", tuya.valueConverter.divideBy10],
            [120, "eco_temperature", tuya.valueConverter.divideBy10],
            [121, "frost_protection_temperature", tuya.valueConverter.divideBy10],
            [122, "frost_protection", tuya.valueConverter.onOff],

            // Unused
            [109, "mode", tuya.valueConverter.text],
            [112, "version", tuya.valueConverter.text],
            [116, "motor_data", tuya.valueConverter.raw],
            [123, "pressure_index", tuya.valueConverter.raw],
            [124, "temporary_mode_starttime", tuya.valueConverter.raw],
            [125, "function_point", tuya.valueConverter.raw],
            [126, "app_features", tuya.valueConverter.raw],
            [127, "system_mode2", tuya.valueConverter.raw],
        ],
    },

};

module.exports = definition;

For future references, here is how it looks in Z2M:

Image Image

And here is how it looks in Home Assistant:

Image Image Image

Levak avatar Dec 14 '25 20:12 Levak

Hi @levak, thanks for your updated converter. I'll take a look on that. 🙏🏼

bernardesarthur avatar Dec 15 '25 15:12 bernardesarthur