From 70cfc2c851088e4b1c39b96bca8fa05a44a7ca67 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Sat, 29 Mar 2025 16:24:16 +0100 Subject: [PATCH] fix: Tuya TYZGTH1CH-D1RF: auto settings --- src/devices/tuya.ts | 100 +------------------------------------------- src/lib/tuya.ts | 62 ++++++++++++++++++++++++++- test/tuya.test.ts | 55 ++++++++++++++++++++++++ test/utils.ts | 1 + 4 files changed, 119 insertions(+), 99 deletions(-) create mode 100644 test/tuya.test.ts diff --git a/src/devices/tuya.ts b/src/devices/tuya.ts index e764346b3fa88..27e1da6047176 100644 --- a/src/devices/tuya.ts +++ b/src/devices/tuya.ts @@ -769,102 +769,6 @@ const fzLocal = { } satisfies Fz.Converter, }; -const modernExtendLocal = { - dpTHZBSettings(): ModernExtend { - const exp = e - .composite("auto_settings", "auto_settings", ea.STATE_SET) - .withFeature(e.enum("enabled", ea.STATE_SET, ["on", "off", "none"]).withDescription("Enable auto settings")) - .withFeature(e.enum("temp_greater_then", ea.STATE_SET, ["on", "off", "none"]).withDescription("Greater action")) - .withFeature( - e - .numeric("temp_greater_value", ea.STATE_SET) - .withValueMin(-20) - .withValueMax(80) - .withValueStep(0.1) - .withUnit("*C") - .withDescription("Temperature greater than value"), - ) - .withFeature(e.enum("temp_lower_then", ea.STATE_SET, ["on", "off", "none"]).withDescription("Lower action")) - .withFeature( - e - .numeric("temp_lower_value", ea.STATE_SET) - .withValueMin(-20) - .withValueMax(80) - .withValueStep(0.1) - .withUnit("*C") - .withDescription("Temperature lower than value"), - ); - - const handlers: [Fz.Converter[], Tz.Converter[]] = tuya.getHandlersForDP("auto_settings", 0x77, tuya.dataTypes.string, { - from: (value: string) => { - let result = { - enabled: "none", - temp_greater_then: "none", - temp_greater_value: 0, - temp_lower_then: "none", - temp_lower_value: 0, - }; - const buf = Buffer.from(value, "hex"); - if (buf.length > 0) { - const enabled = buf[0]; - const gr = buf[1]; - const grValue = buf.readInt32LE(2) / 10; - const grAction = buf[6]; - const lo = buf[7]; - const loValue = buf.readInt32LE(8) / 10; - const loAction = buf[13]; - result = { - enabled: {0: "on", 128: "off"}[enabled], - temp_greater_then: gr !== 0xff ? {1: "on", 0: "off"}[grAction] : "none", - temp_greater_value: grValue, - temp_lower_then: lo !== 0xff ? {1: "on", 0: "off"}[loAction] : "none", - temp_lower_value: loValue, - }; - } - return result; - }, - to: (value: KeyValueAny) => { - let result = ""; - if (value.enabled !== "none") { - const enabled = utils.getFromLookup(value.enabled, { - on: 0x00, - off: 0x80, - }); - const gr = value.temp_greater_then === "none" ? 0xff : 0x00; - const grAction = utils.getFromLookup(value.temp_greater_then, { - on: 0x01, - off: 0x00, - none: 0x00, - }); - const lo = value.temp_lower_then === "none" ? 0xff : 0x00; - const loAction = utils.getFromLookup(value.temp_lower_then, { - on: 0x01, - off: 0x00, - none: 0x00, - }); - const buf = Buffer.alloc(13); - buf.writeUInt8(enabled, 0); - buf.writeUInt8(gr, 1); - buf.writeInt32LE(value.temp_greater_value * 10, 2); - buf.writeUInt8(grAction, 6); - buf.writeUInt8(lo, 7); - buf.writeInt32LE(value.temp_lower_value * 10, 8); - buf.writeUInt8(loAction, 12); - result = buf.toString("hex"); - } - return result; - }, - }); - - return { - exposes: [exp], - fromZigbee: handlers[0], - toZigbee: handlers[1], - isModernExtend: true, - }; - }, -}; - export const definitions: DefinitionWithExtend[] = [ { zigbeeModel: ["TS0204"], @@ -14364,9 +14268,9 @@ export const definitions: DefinitionWithExtend[] = [ type: tuya.dataTypes.enum, valueOn: ["ON", 1], valueOff: ["OFF", 0], - description: "Manual mode or automatic", + description: "Manual mode, ON = auto settings disabled, OFF = auto settings enabled", }), - modernExtendLocal.dpTHZBSettings(), + tuya.modernExtend.dpTHZBSettings(), ], }, { diff --git a/src/lib/tuya.ts b/src/lib/tuya.ts index 7835d89448d56..ac88c90dd17d7 100644 --- a/src/lib/tuya.ts +++ b/src/lib/tuya.ts @@ -269,7 +269,7 @@ function dpValueFromEnum(dp: number, value: number) { return {dp, datatype: dataTypes.enum, data: [value]}; } -function dpValueFromString(dp: number, string: string) { +export function dpValueFromString(dp: number, string: string) { return {dp, datatype: dataTypes.string, data: convertStringToHexArray(string)}; } @@ -2072,6 +2072,66 @@ export interface TuyaDPLightArgs { } const tuyaModernExtend = { + dpTHZBSettings(): ModernExtend { + const exp = e + .composite("auto_settings", "auto_settings", ea.STATE_SET) + .withDescription("Automatically switch ON/OFF, make sure manual mode is turned OFF otherwise auto settings are not applied.") + .withFeature(e.binary("enabled", ea.STATE_SET, true, false).withDescription("Enable auto settings")) + .withFeature(e.enum("temp_greater_then", ea.STATE_SET, ["ON", "OFF"]).withDescription("Greater action")) + .withFeature( + e + .numeric("temp_greater_value", ea.STATE_SET) + .withValueMin(-20) + .withValueMax(80) + .withValueStep(0.1) + .withUnit("°C") + .withDescription("Temperature greater than value"), + ) + .withFeature(e.enum("temp_lower_then", ea.STATE_SET, ["ON", "OFF"]).withDescription("Lower action")) + .withFeature( + e + .numeric("temp_lower_value", ea.STATE_SET) + .withValueMin(-20) + .withValueMax(80) + .withValueStep(0.1) + .withUnit("°C") + .withDescription("Temperature lower than value"), + ); + + const handlers: [Fz.Converter[], Tz.Converter[]] = getHandlersForDP("auto_settings", 0x77, dataTypes.string, { + from: (value: string) => { + const buffer = Buffer.from(value, "hex"); + if (buffer.length > 0) { + return { + enabled: buffer.readUint16LE(0) === 0x80, + temp_greater_value: buffer.readInt32LE(2) / 10, + temp_greater_then: buffer.readUint8(6) ? "ON" : "OFF", + temp_lower_value: buffer.readInt32LE(8) / 10, + temp_lower_then: buffer.readUint8(12) ? "ON" : "OFF", + }; + } + }, + to: async (value: KeyValueAny, meta) => { + const buffer = Buffer.alloc(13); + buffer.writeUint16LE(value.enabled ? 0x80 : 0x00, 0); + buffer.writeInt32LE(value.temp_greater_value * 10, 2); + buffer.writeUInt8(value.temp_greater_then === "ON" ? 1 : 0, 6); + buffer.writeUInt8(1, 7); + buffer.writeInt32LE(value.temp_lower_value * 10, 8); + buffer.writeUInt8(value.temp_lower_then === "ON" ? 1 : 0, 12); + // Disable manual mode, otherwise auto settings is not applied. + await sendDataPointEnum(meta.device.endpoints[0], 0x65, 0, "sendData", 1); + return buffer.toString("hex"); + }, + }); + + return { + exposes: [exp], + fromZigbee: handlers[0], + toZigbee: handlers[1], + isModernExtend: true, + }; + }, tuyaBase(args?: {onEvent?: OnEventArgs; dp: true}): ModernExtend { const result: ModernExtend = { configure: [configureMagicPacket], diff --git a/test/tuya.test.ts b/test/tuya.test.ts new file mode 100644 index 0000000000000..45b97eb4d09b5 --- /dev/null +++ b/test/tuya.test.ts @@ -0,0 +1,55 @@ +import type {Fz} from "src/lib/types"; +import {type Tz, findByDevice} from "../src/index"; +import * as tuya from "../src/lib/tuya"; +import {mockDevice} from "./utils"; + +describe("lib/tuya", () => { + describe("dpTHZBSettings", async () => { + const {toZigbee, fromZigbee} = tuya.modernExtend.dpTHZBSettings(); + const device = mockDevice({modelID: "TS000F", manufacturerName: "_TZ3218_7fiyo3kv", endpoints: [{}]}); + const definition = await findByDevice(device); + + // 0000 disable writeInt32LE(temp_greater_value * 10) 01 on unknown writeInt32LE(temp_lower_value * 10) 01 on + // 8000 enable 00 off 01 00 off + // 2 bytes 4 bytes 1 byte 1 byte 4 byte 1 byte + + const enable20OnMinus10Off = { + to: tuya.dpValueFromString(119, "8000" + "c8000000" + "0101" + "9cffffff" + "00"), + from: {auto_settings: {enabled: true, temp_greater_then: "ON", temp_greater_value: 20, temp_lower_value: -10, temp_lower_then: "OFF"}}, + }; + + const disable0Off0Dot2On = { + to: tuya.dpValueFromString(119, "0000" + "00000000" + "0001" + "02000000" + "01"), + from: {auto_settings: {enabled: false, temp_greater_then: "OFF", temp_greater_value: 0, temp_lower_value: 0.2, temp_lower_then: "ON"}}, + }; + + it.each([enable20OnMinus10Off, disable0Off0Dot2On])("toZigbee", async (data) => { + const meta: Tz.Meta = {state: {}, device, message: null, mapped: definition, options: null, publish: null, endpoint_name: null}; + await toZigbee[0].convertSet(device.endpoints[0], "auto_settings", data.from.auto_settings, { + ...meta, + message: data.from, + }); + // Should disable manual mode + expect(device.endpoints[0].command).toHaveBeenNthCalledWith( + 1, + "manuSpecificTuya", + "sendData", + {seq: 1, dpValues: [{data: [0], datatype: 4, dp: 101}]}, + {disableDefaultResponse: true}, + ); + expect(device.endpoints[0].command).toHaveBeenNthCalledWith( + 2, + "manuSpecificTuya", + "sendData", + {seq: 1, dpValues: [data.to]}, + {disableDefaultResponse: true}, + ); + }); + + it.each([enable20OnMinus10Off, disable0Off0Dot2On])("fromZigbee", async (data) => { + const msg = {data: {dpValues: [data.to]}} as Fz.Message; + const result = await fromZigbee[0].convert(definition, msg, null, null, null); + expect(result).toStrictEqual(data.from); + }); + }); +}); diff --git a/test/utils.ts b/test/utils.ts index d050bfd19f14a..eb28520a52659 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -72,6 +72,7 @@ function mockEndpoint(args: MockEndpointArgs, device: Zh.Device | undefined): Zh bind: vi.fn(), configureReporting: vi.fn(), read: vi.fn(), + command: vi.fn(), getDevice: () => device, inputClusters, outputClusters,