UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

1,027 lines 118 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.clusters = exports.modernExtend = exports.fz = exports.tz = exports.valueConverter = exports.valueConverterBasic = exports.Bitmap = exports.enum = exports.Enum = exports.whitelabel = exports.fingerprint = exports.configureMagicPacket = exports.skip = exports.exposes = exports.dataTypes = void 0; exports.convertBufferToNumber = convertBufferToNumber; exports.onEvent = onEvent; exports.convertDecimalValueTo4ByteHexArray = convertDecimalValueTo4ByteHexArray; exports.onEventMeasurementPoll = onEventMeasurementPoll; exports.onEventSetTime = onEventSetTime; exports.onEventSetLocalTime = onEventSetLocalTime; exports.dpValueFromString = dpValueFromString; exports.sendDataPointValue = sendDataPointValue; exports.sendDataPointBool = sendDataPointBool; exports.sendDataPointEnum = sendDataPointEnum; exports.sendDataPointRaw = sendDataPointRaw; exports.sendDataPointBitmap = sendDataPointBitmap; exports.sendDataPointStringBuffer = sendDataPointStringBuffer; exports.getHandlersForDP = getHandlersForDP; const zigbee_herdsman_1 = require("zigbee-herdsman"); const fz = __importStar(require("../converters/fromZigbee")); const tz = __importStar(require("../converters/toZigbee")); const constants = __importStar(require("./constants")); const exposes = __importStar(require("./exposes")); const logger_1 = require("./logger"); const modernExtend = __importStar(require("./modernExtend")); const globalStore = __importStar(require("./store")); const utils = __importStar(require("./utils")); const utils_1 = require("./utils"); // import {Color} from './color'; const NS = "zhc:tuya"; const e = exposes.presets; const ea = exposes.access; exports.dataTypes = { raw: 0, // [ bytes ] bool: 1, // [0/1] number: 2, // [ 4 byte value ] string: 3, // [ N byte string ] enum: 4, // [ 0-255 ] bitmap: 5, // [ 1,2,4 bytes ] as bits }; function convertBufferToNumber(chunks) { let value = 0; for (let i = 0; i < chunks.length; i++) { value = value << 8; value += chunks[i]; } return value; } function convertStringToHexArray(value) { const asciiKeys = []; for (let i = 0; i < value.length; i++) { asciiKeys.push(value[i].charCodeAt(0)); } return asciiKeys; } function onEvent(args) { return async (type, data, device, settings, state) => { // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` args = { queryOnDeviceAnnounce: false, timeStart: "1970", respondToMcuVersionResponse: true, ...args }; const endpoint = device.endpoints[0]; if (type === "message" && data.cluster === "manuSpecificTuya") { if (args.respondToMcuVersionResponse && data.type === "commandMcuVersionResponse") { await endpoint.command("manuSpecificTuya", "mcuVersionRequest", { seq: 0x0002 }); } else if (data.type === "commandMcuGatewayConnectionStatus") { // "payload" can have the following values: // 0x00: The gateway is not connected to the internet. // 0x01: The gateway is connected to the internet. // 0x02: The request timed out after three seconds. const payload = { payloadSize: 1, payload: 1 }; await endpoint.command("manuSpecificTuya", "mcuGatewayConnectionStatus", payload, {}); } } if (data.type === "commandMcuSyncTime" && data.cluster === "manuSpecificTuya") { try { const offset = args.timeStart === "2000" ? constants.OneJanuary2000 : 0; const utcTime = Math.round((new Date().getTime() - offset) / 1000); const localTime = utcTime - new Date().getTimezoneOffset() * 60; const payload = { payloadSize: 8, payload: [...convertDecimalValueTo4ByteHexArray(utcTime), ...convertDecimalValueTo4ByteHexArray(localTime)], }; await endpoint.command("manuSpecificTuya", "mcuSyncTime", payload, {}); } catch { /* handle error to prevent crash */ } } // Some devices require a dataQuery on deviceAnnounce, otherwise they don't report any data if (args.queryOnDeviceAnnounce && type === "deviceAnnounce") { await endpoint.command("manuSpecificTuya", "dataQuery", {}); } if (args.queryIntervalSeconds) { if (type === "stop") { clearTimeout(globalStore.getValue(device, "query_interval")); globalStore.clearValue(device, "query_interval"); } else if (!globalStore.hasValue(device, "query_interval")) { const setTimer = () => { const timer = setTimeout(async () => { try { await endpoint.command("manuSpecificTuya", "dataQuery", {}); } catch { /* Do nothing*/ } setTimer(); }, args.queryIntervalSeconds * 1000); globalStore.putValue(device, "query_interval", timer); }; setTimer(); } } }; } function getDataValue(dpValue) { let dataString = ""; switch (dpValue.datatype) { case exports.dataTypes.raw: return dpValue.data; case exports.dataTypes.bool: return dpValue.data[0] === 1; case exports.dataTypes.number: return convertBufferToNumber(dpValue.data); case exports.dataTypes.string: // Don't use .map here, doesn't work: https://github.com/Koenkk/zigbee-herdsman-converters/pull/1799/files#r530377091 for (let i = 0; i < dpValue.data.length; ++i) { dataString += String.fromCharCode(dpValue.data[i]); } return dataString; case exports.dataTypes.enum: return dpValue.data[0]; case exports.dataTypes.bitmap: return convertBufferToNumber(dpValue.data); } } function convertDecimalValueTo4ByteHexArray(value) { const hexValue = Number(value).toString(16).padStart(8, "0"); const chunk1 = hexValue.substring(0, 2); const chunk2 = hexValue.substring(2, 4); const chunk3 = hexValue.substring(4, 6); const chunk4 = hexValue.substring(6); return [chunk1, chunk2, chunk3, chunk4].map((hexVal) => Number.parseInt(hexVal, 16)); } function convertDecimalValueTo2ByteHexArray(value) { const hexValue = Number(value).toString(16).padStart(4, "0"); const chunk1 = hexValue.substring(0, 2); const chunk2 = hexValue.substring(2); return [chunk1, chunk2].map((hexVal) => Number.parseInt(hexVal, 16)); } function onEventMeasurementPoll(type, data, device, options, electricalMeasurement = true, metering = false) { const endpoint = device.getEndpoint(1); const poll = async () => { if (electricalMeasurement) { await endpoint.read("haElectricalMeasurement", ["rmsVoltage", "rmsCurrent", "activePower"]); } if (metering) { await endpoint.read("seMetering", ["currentSummDelivered"]); } }; utils.onEventPoll(type, data, device, options, "measurement", 60, poll); } async function onEventSetTime(type, data, device) { // FIXME: Need to join onEventSetTime/onEventSetLocalTime to one command if (data.type === "commandMcuSyncTime" && data.cluster === "manuSpecificTuya") { try { const utcTime = Math.round((new Date().getTime() - constants.OneJanuary2000) / 1000); const localTime = utcTime - new Date().getTimezoneOffset() * 60; const endpoint = device.getEndpoint(1); const payload = { payloadSize: 8, payload: [...convertDecimalValueTo4ByteHexArray(utcTime), ...convertDecimalValueTo4ByteHexArray(localTime)], }; await endpoint.command("manuSpecificTuya", "mcuSyncTime", payload, {}); } catch { // endpoint.command can throw an error which needs to // be caught or the zigbee-herdsman may crash // Debug message is handled in the zigbee-herdsman } } } // set UTC and Local Time as total number of seconds from 00: 00: 00 on January 01, 1970 // force to update every device time every hour due to very poor clock async function onEventSetLocalTime(type, data, device) { // FIXME: What actually nextLocalTimeUpdate/forceTimeUpdate do? // I did not find any timers or something else where it was used. // Actually, there are two ways to set time on Tuya MCU devices: // 1. Respond to the `commandMcuSyncTime` event // 2. Just send `mcuSyncTime` anytime (by 1-hour timer or something else) const nextLocalTimeUpdate = globalStore.getValue(device, "nextLocalTimeUpdate"); const forceTimeUpdate = nextLocalTimeUpdate == null || nextLocalTimeUpdate < new Date().getTime(); if ((data.type === "commandMcuSyncTime" && data.cluster === "manuSpecificTuya") || forceTimeUpdate) { globalStore.putValue(device, "nextLocalTimeUpdate", new Date().getTime() + 3600 * 1000); try { const utcTime = Math.round(new Date().getTime() / 1000); const localTime = utcTime - new Date().getTimezoneOffset() * 60; const endpoint = device.getEndpoint(1); const payload = { payloadSize: 8, payload: [...convertDecimalValueTo4ByteHexArray(utcTime), ...convertDecimalValueTo4ByteHexArray(localTime)], }; await endpoint.command("manuSpecificTuya", "mcuSyncTime", payload, {}); } catch { // endpoint.command can throw an error which needs to // be caught or the zigbee-herdsman may crash // Debug message is handled in the zigbee-herdsman } } } // Return `seq` - transaction ID for handling concrete response async function sendDataPoints(entity, dpValues, cmd = "dataRequest", seq) { if (seq === undefined) { // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` seq = globalStore.getValue(entity, "sequence", 0); globalStore.putValue(entity, "sequence", (seq + 1) % 0xffff); } await entity.command("manuSpecificTuya", cmd, { seq, dpValues }, { disableDefaultResponse: true }); return seq; } function dpValueFromNumberValue(dp, value) { return { dp, datatype: exports.dataTypes.number, data: convertDecimalValueTo4ByteHexArray(value) }; } function dpValueFromBool(dp, value) { return { dp, datatype: exports.dataTypes.bool, data: [value ? 1 : 0] }; } function dpValueFromEnum(dp, value) { return { dp, datatype: exports.dataTypes.enum, data: [value] }; } function dpValueFromString(dp, string) { return { dp, datatype: exports.dataTypes.string, data: convertStringToHexArray(string) }; } function dpValueFromRaw(dp, rawBuffer) { return { dp, datatype: exports.dataTypes.raw, data: rawBuffer }; } function dpValueFromBitmap(dp, bitmapBuffer) { return { dp, datatype: exports.dataTypes.bitmap, data: [bitmapBuffer] }; } async function sendDataPointValue(entity, dp, value, cmd, seq) { return await sendDataPoints(entity, [dpValueFromNumberValue(dp, value)], cmd, seq); } async function sendDataPointBool(entity, dp, value, cmd, seq) { return await sendDataPoints(entity, [dpValueFromBool(dp, value)], cmd, seq); } async function sendDataPointEnum(entity, dp, value, cmd, seq) { return await sendDataPoints(entity, [dpValueFromEnum(dp, value)], cmd, seq); } async function sendDataPointRaw(entity, dp, value, cmd, seq) { return await sendDataPoints(entity, [dpValueFromRaw(dp, value)], cmd, seq); } async function sendDataPointBitmap(entity, dp, value, cmd, seq) { return await sendDataPoints(entity, [dpValueFromBitmap(dp, value)], cmd, seq); } async function sendDataPointStringBuffer(entity, dp, value, cmd, seq) { return await sendDataPoints(entity, [dpValueFromString(dp, value)], cmd, seq); } const tuyaExposes = { lightType: () => e.enum("light_type", ea.STATE_SET, ["led", "incandescent", "halogen"]).withDescription("Type of light attached to the device"), lightBrightnessWithMinMax: () => e .light_brightness() .withMinBrightness() .withMaxBrightness() .setAccess("state", ea.STATE_SET) .setAccess("brightness", ea.STATE_SET) .setAccess("min_brightness", ea.STATE_SET) .setAccess("max_brightness", ea.STATE_SET), lightBrightness: () => e.light_brightness().setAccess("state", ea.STATE_SET).setAccess("brightness", ea.STATE_SET), countdown: () => e .numeric("countdown", ea.STATE_SET) .withValueMin(0) .withValueMax(43200) .withValueStep(1) .withUnit("s") .withDescription("Countdown to turn device off after a certain time"), switch: () => e.switch().setAccess("state", ea.STATE_SET), selfTest: () => e.binary("self_test", ea.STATE_SET, true, false).withDescription("Indicates whether the device is being self-tested"), selfTestResult: () => e.enum("self_test_result", ea.STATE, ["checking", "success", "failure", "others"]).withDescription("Result of the self-test"), faultAlarm: () => e.binary("fault_alarm", ea.STATE, true, false).withDescription("Indicates whether a fault was detected"), silence: () => e.binary("silence", ea.STATE_SET, true, false).withDescription("Silence the alarm"), frostProtection: (extraNote = "") => e .binary("frost_protection", ea.STATE_SET, "ON", "OFF") .withDescription(`When Anti-Freezing function is activated, the temperature in the house is kept at 8 °C.${extraNote}`), errorStatus: () => e.numeric("error_status", ea.STATE).withDescription("Error status"), scheduleAllDays: (access, example) => ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"].map((day) => e.text(`schedule_${day}`, access).withDescription(`Schedule for ${day}, example: "${example}"`)), temperatureUnit: () => e.enum("temperature_unit", ea.STATE_SET, ["celsius", "fahrenheit"]).withDescription("Temperature unit"), temperatureCalibration: () => e .numeric("temperature_calibration", ea.STATE_SET) .withValueMin(-2.0) .withValueMax(2.0) .withValueStep(0.1) .withUnit("°C") .withDescription("Temperature calibration"), humidityCalibration: () => e .numeric("humidity_calibration", ea.STATE_SET) .withValueMin(-30) .withValueMax(30) .withValueStep(1) .withUnit("%") .withDescription("Humidity calibration"), soilCalibration: () => e .numeric("soil_calibration", ea.STATE_SET) .withValueMin(-30) .withValueMax(30) .withValueStep(1) .withUnit("%") .withDescription("Soil Humidity calibration"), temperatureSampling: () => e .numeric("temperature_sampling", ea.STATE_SET) .withValueMin(5) .withValueMax(3600) .withValueStep(1) .withUnit("s") .withDescription("Air temperature and humidity sampling"), soilSampling: () => e .numeric("soil_sampling", ea.STATE_SET) .withValueMin(5) .withValueMax(3600) .withValueStep(1) .withUnit("s") .withDescription("Soil humidity sampling"), soilWarning: () => e .numeric("soil_warning", ea.STATE_SET) .withValueMin(0) .withValueMax(100) .withValueStep(1) .withUnit("%") .withDescription("Soil water shortage humidity value"), gasValue: () => e.numeric("gas_value", ea.STATE).withDescription("Measured gas concentration"), energyWithPhase: (phase) => e.numeric(`energy_${phase}`, ea.STATE).withUnit("kWh").withDescription(`Sum of consumed energy (phase ${phase.toUpperCase()})`), energyProducedWithPhase: (phase) => e.numeric(`energy_produced_${phase}`, ea.STATE).withUnit("kWh").withDescription(`Sum of produced energy (phase ${phase.toUpperCase()})`), energyFlowWithPhase: (phase, more) => e .enum(`energy_flow_${phase}`, ea.STATE, ["consuming", "producing", ...more]) .withDescription(`Direction of energy (phase ${phase.toUpperCase()})`), voltageWithPhase: (phase) => e.numeric(`voltage_${phase}`, ea.STATE).withUnit("V").withDescription(`Measured electrical potential value (phase ${phase.toUpperCase()})`), powerWithPhase: (phase) => e.numeric(`power_${phase}`, ea.STATE).withUnit("W").withDescription(`Instantaneous measured power (phase ${phase.toUpperCase()})`), currentWithPhase: (phase) => e .numeric(`current_${phase}`, ea.STATE) .withUnit("A") .withDescription(`Instantaneous measured electrical current (phase ${phase.toUpperCase()})`), powerFactorWithPhase: (phase) => e .numeric(`power_factor_${phase}`, ea.STATE) .withUnit("%") .withDescription(`Instantaneous measured power factor (phase ${phase.toUpperCase()})`), switchType: () => e.enum("switch_type", ea.ALL, ["toggle", "state", "momentary"]).withDescription("Type of the switch"), backlightModeLowMediumHigh: () => e.enum("backlight_mode", ea.ALL, ["low", "medium", "high"]).withDescription("Intensity of the backlight"), backlightModeOffNormalInverted: () => e.enum("backlight_mode", ea.ALL, ["off", "normal", "inverted"]).withDescription("Mode of the backlight"), backlightModeOffOn: () => e.binary("backlight_mode", ea.ALL, "ON", "OFF").withDescription("Mode of the backlight"), indicatorMode: () => e.enum("indicator_mode", ea.ALL, ["off", "off/on", "on/off", "on"]).withDescription("LED indicator mode"), indicatorModeNoneRelayPos: () => e.enum("indicator_mode", ea.ALL, ["none", "relay", "pos"]).withDescription("Mode of the indicator light"), powerOutageMemory: () => e.enum("power_outage_memory", ea.ALL, ["on", "off", "restore"]).withDescription("Recover state after power outage"), batteryState: () => e.enum("battery_state", ea.STATE, ["low", "medium", "high"]).withDescription("State of the battery"), doNotDisturb: () => e .binary("do_not_disturb", ea.STATE_SET, true, false) .withDescription("Do not disturb mode, when enabled this function will keep the light OFF after a power outage"), colorPowerOnBehavior: () => e.enum("color_power_on_behavior", ea.STATE_SET, ["initial", "previous", "customized"]).withDescription("Power on behavior state"), switchMode: () => e.enum("switch_mode", ea.STATE_SET, ["switch", "scene"]).withDescription("Sets the mode of the switch to act as a switch or as a scene"), switchMode2: () => e .enum("switch_mode", ea.STATE_SET, ["switch", "curtain"]) .withDescription("Sets the mode of the switch to act as a switch or as a curtain controller"), lightMode: () => e.enum("light_mode", ea.STATE_SET, ["normal", "on", "off", "flash"]).withDescription(`'Sets the indicator mode of l1. Normal: Orange while off and white while on. On: Always white. Off: Always orange. Flash: Flashes white when triggered. Note: Orange light will turn off after light off delay, white light always stays on. Light mode updates on next state change.'`), // Inching can be enabled for multiple endpoints (1 to 6) but it is always controlled on endpoint 1 // So instead of pinning the values to each endpoint, it is easier to keep the structure stand alone. inchingSwitch: (quantity) => { const x = e .composite("inching_control_set", "inching_control_set", ea.SET) .withDescription("Device Inching function Settings. The device will automatically turn off " + "after each turn on for a specified period of time."); for (let i = 1; i <= quantity; i++) { x.withFeature(e .binary("inching_control", ea.SET, "ENABLE", "DISABLE") .withDescription(`Enable/disable inching function for endpoint ${i}.`) .withLabel(`Inching for Endpoint ${i}`) .withProperty(`inching_control_${i}`)).withFeature(e .numeric("inching_time", ea.SET) .withDescription(`Delay time for executing a inching action for endpoint ${i}.`) .withLabel(`Inching time for endpoint ${i}`) .withProperty(`inching_time_${i}`) .withUnit("seconds") .withValueMin(1) .withValueMax(65535) .withValueStep(1)); } return x; }, }; exports.exposes = tuyaExposes; exports.skip = { // Prevent state from being published when already ON and brightness is also published. // This prevents 100% -> X% brightness jumps when the switch is already on // https://github.com/Koenkk/zigbee2mqtt/issues/13800#issuecomment-1263592783 stateOnAndBrightnessPresent: (meta) => { if (Array.isArray(meta.mapped)) throw new Error("Not supported"); const convertedKey = meta.mapped.meta.multiEndpoint && meta.endpoint_name ? `state_${meta.endpoint_name}` : "state"; return meta.message.brightness != null && meta.state[convertedKey] === meta.message.state; }, }; const configureMagicPacket = async (device, coordinatorEndpoint) => { try { const endpoint = device.endpoints[0]; await endpoint.read("genBasic", ["manufacturerName", "zclVersion", "appVersion", "modelId", "powerSource", 0xfffe]); } catch (e) { // Fails for some Tuya devices with UNSUPPORTED_ATTRIBUTE, ignore that. // e.g. https://github.com/Koenkk/zigbee2mqtt/issues/14857 if (e.message.includes("UNSUPPORTED_ATTRIBUTE")) { logger_1.logger.debug("configureMagicPacket failed, ignoring...", NS); } else { throw e; } } }; exports.configureMagicPacket = configureMagicPacket; const fingerprint = (modelID, manufacturerNames) => { return manufacturerNames.map((manufacturerName) => { return { modelID, manufacturerName }; }); }; exports.fingerprint = fingerprint; const whitelabel = (vendor, model, description, manufacturerNames) => { const fingerprint = manufacturerNames.map((manufacturerName) => { return { manufacturerName }; }); return { vendor, model, description, fingerprint }; }; exports.whitelabel = whitelabel; class Base { value; constructor(value) { this.value = value; } valueOf() { return this.value; } } class Enum extends Base { } exports.Enum = Enum; const enumConstructor = (value) => new Enum(value); exports.enum = enumConstructor; class Bitmap extends Base { } exports.Bitmap = Bitmap; exports.valueConverterBasic = { lookup: (map, fallbackValue) => { return { to: (v, meta) => utils.getFromLookup(v, typeof map === "function" ? map(meta.options, meta.device) : map), from: (v, _meta, options) => { const m = typeof map === "function" ? map(options, _meta.device) : map; const value = Object.entries(m).find((i) => i[1].valueOf() === v); if (!value) { if (fallbackValue !== undefined) return fallbackValue; throw new Error(`Value '${v}' is not allowed, expected one of ${Object.values(m).map((i) => i.valueOf())}`); } return value[0]; }, }; }, scale: (min1, max1, min2, max2) => { return { to: (v) => utils.mapNumberRange(v, min1, max1, min2, max2), from: (v) => utils.mapNumberRange(v, min2, max2, min1, max1), }; }, raw: () => { return { to: (v) => v, from: (v) => v }; }, divideBy: (value) => { return { to: (v) => v * value, from: (v) => v / value }; }, divideByFromOnly: (value) => { return { to: (v) => v, from: (v) => v / value }; }, trueFalse: (valueTrue) => { return { from: (v) => v === valueTrue.valueOf() }; }, }; exports.valueConverter = { trueFalse0: exports.valueConverterBasic.trueFalse(0), trueFalse1: exports.valueConverterBasic.trueFalse(1), trueFalseInvert: { to: (v) => !v, from: (v) => !v, }, trueFalseEnum0: exports.valueConverterBasic.trueFalse(new Enum(0)), trueFalseEnum1: exports.valueConverterBasic.trueFalse(new Enum(1)), onOff: exports.valueConverterBasic.lookup({ ON: true, OFF: false }), powerOnBehavior: exports.valueConverterBasic.lookup({ off: 0, on: 1, previous: 2 }), powerOnBehaviorEnum: exports.valueConverterBasic.lookup({ off: new Enum(0), on: new Enum(1), previous: new Enum(2) }), switchType: exports.valueConverterBasic.lookup({ momentary: new Enum(0), toggle: new Enum(1), state: new Enum(2) }), switchType2: exports.valueConverterBasic.lookup({ toggle: new Enum(0), state: new Enum(1), momentary: new Enum(2) }), backlightModeOffNormalInverted: exports.valueConverterBasic.lookup({ off: new Enum(0), normal: new Enum(1), inverted: new Enum(2) }), backlightModeOffLowMediumHigh: exports.valueConverterBasic.lookup({ off: new Enum(0), low: new Enum(1), medium: new Enum(2), high: new Enum(3) }), lightType: exports.valueConverterBasic.lookup({ led: 0, incandescent: 1, halogen: 2 }), countdown: exports.valueConverterBasic.raw(), scale0_254to0_1000: exports.valueConverterBasic.scale(0, 254, 0, 1000), scale0_1to0_1000: exports.valueConverterBasic.scale(0, 1, 0, 1000), temperatureUnit: exports.valueConverterBasic.lookup({ celsius: 0, fahrenheit: 1 }), temperatureUnitEnum: exports.valueConverterBasic.lookup({ celsius: new Enum(0), fahrenheit: new Enum(1) }), batteryState: exports.valueConverterBasic.lookup({ low: 0, medium: 1, high: 2 }), divideBy2: exports.valueConverterBasic.divideBy(2), divideBy10: exports.valueConverterBasic.divideBy(10), divideBy100: exports.valueConverterBasic.divideBy(100), divideBy1000: exports.valueConverterBasic.divideBy(1000), divideBy10FromOnly: exports.valueConverterBasic.divideByFromOnly(10), switchMode: exports.valueConverterBasic.lookup({ switch: new Enum(0), scene: new Enum(1) }), switchMode2: exports.valueConverterBasic.lookup({ switch: new Enum(0), curtain: new Enum(1) }), lightMode: exports.valueConverterBasic.lookup({ normal: new Enum(0), on: new Enum(1), off: new Enum(2), flash: new Enum(3) }), raw: exports.valueConverterBasic.raw(), localTemperatureCalibration: { from: (value) => (value > 4000 ? value - 4096 : value), to: (value) => (value < 0 ? 4096 + value : value), }, // biome-ignore lint/style/useNamingConvention: ignored using `--suppress` localTemperatureCalibration_256: { from: (value) => (value > 200 ? value - 256 : value), to: (value) => (value < 0 ? 256 + value : value), }, setLimit: { to: (v) => { if (!v) throw new Error("Limit cannot be unset, use factory_reset"); return v; }, from: (v) => v, }, coverPosition: { to: (v, meta) => { return meta.options.invert_cover ? 100 - v : v; }, from: (v, meta, options, publish) => { const position = options.invert_cover ? 100 - v : v; publish({ state: position === 0 ? "CLOSE" : "OPEN" }); return position; }, }, coverPositionInverted: { to: (v, meta) => { return meta.options.invert_cover ? v : 100 - v; }, from: (v, meta, options, publish) => { const position = options.invert_cover ? v : 100 - v; publish({ state: position === 0 ? "CLOSE" : "OPEN" }); return position; }, }, tubularMotorDirection: exports.valueConverterBasic.lookup({ normal: new Enum(0), reversed: new Enum(1) }), plus1: { from: (v) => v + 1, to: (v) => v - 1, }, static: (value) => { return { from: (v) => { return value; }, }; }, phaseVariant1: { from: (v) => { const buffer = Buffer.from(v, "base64"); return { voltage: (buffer[14] | (buffer[13] << 8)) / 10, current: (buffer[12] | (buffer[11] << 8)) / 1000 }; }, }, phaseVariant2: { from: (v) => { const buf = Buffer.from(v, "base64"); return { voltage: (buf[1] | (buf[0] << 8)) / 10, current: (buf[4] | (buf[3] << 8)) / 1000, power: buf[7] | (buf[6] << 8) }; }, }, phaseVariant2WithPhase: (phase) => { return { from: (v) => { // Support negative power readings // https://github.com/Koenkk/zigbee2mqtt/issues/18603#issuecomment-2277697295 const buf = Buffer.from(v, "base64"); let power = buf[7] | (buf[6] << 8); if (power > 0x7fff) { power = (0x999a - power) * -1; } return { [`voltage_${phase}`]: (buf[1] | (buf[0] << 8)) / 10, [`current_${phase}`]: (buf[4] | (buf[3] << 8)) / 1000, [`power_${phase}`]: power, }; }, }; }, phaseVariant3: { from: (v) => { const buf = Buffer.from(v, "base64"); return { voltage: ((buf[0] << 8) | buf[1]) / 10, current: ((buf[2] << 16) | (buf[3] << 8) | buf[4]) / 1000, power: (buf[5] << 16) | (buf[6] << 8) | buf[7], }; }, }, power: { from: (v) => { // Support negative readings // https://github.com/Koenkk/zigbee2mqtt/issues/18603 return v > 0x0fffffff ? (0x1999999c - v) * -1 : v; }, }, threshold: { from: (v) => { const buffer = Buffer.from(v, "base64"); const stateLookup = { 0: "not_set", 1: "over_current_threshold", 3: "over_voltage_threshold" }; const protectionLookup = { 0: "OFF", 1: "ON" }; return { threshold_1_protection: protectionLookup[buffer[1]], threshold_1: stateLookup[buffer[0]], threshold_1_value: buffer[3] | (buffer[2] << 8), threshold_2_protection: protectionLookup[buffer[5]], threshold_2: stateLookup[buffer[4]], threshold_2_value: buffer[7] | (buffer[6] << 8), }; }, }, threshold_2: { to: async (v, meta) => { const entity = meta.device.endpoints[0]; const onOffLookup = { on: 1, off: 0 }; const sendCommand = utils.getMetaValue(entity, meta.mapped, "tuyaSendCommand", undefined, "dataRequest"); if (meta.message.overload_breaker) { const threshold = meta.state.overload_threshold; const buf = Buffer.from([ 3, utils.getFromLookup(meta.message.overload_breaker, onOffLookup), 0, utils.toNumber(threshold, "overload_threshold"), ]); await sendDataPointRaw(entity, 17, Array.from(buf), sendCommand, 1); } else if (meta.message.overload_threshold) { const state = meta.state.overload_breaker; const buf = Buffer.from([ 3, utils.getFromLookup(state, onOffLookup), 0, utils.toNumber(meta.message.overload_threshold, "overload_threshold"), ]); await sendDataPointRaw(entity, 17, Array.from(buf), sendCommand, 1); } else if (meta.message.leakage_threshold) { const state = meta.state.leakage_breaker; const buf = Buffer.alloc(8); buf.writeUInt8(4, 4); buf.writeUInt8(utils.getFromLookup(state, onOffLookup), 5); buf.writeUInt16BE(utils.toNumber(meta.message.leakage_threshold, "leakage_threshold"), 6); await sendDataPointRaw(entity, 17, Array.from(buf), sendCommand, 1); } else if (meta.message.leakage_breaker) { const threshold = meta.state.leakage_threshold; const buf = Buffer.alloc(8); buf.writeUInt8(4, 4); buf.writeUInt8(utils.getFromLookup(meta.message.leakage_breaker, onOffLookup), 5); buf.writeUInt16BE(utils.toNumber(threshold, "leakage_threshold"), 6); await sendDataPointRaw(entity, 17, Array.from(buf), sendCommand, 1); } else if (meta.message.high_temperature_threshold) { const state = meta.state.high_temperature_breaker; const buf = Buffer.alloc(12); buf.writeUInt8(5, 8); buf.writeUInt8(utils.getFromLookup(state, onOffLookup), 9); buf.writeUInt16BE(utils.toNumber(meta.message.high_temperature_threshold, "high_temperature_threshold"), 10); await sendDataPointRaw(entity, 17, Array.from(buf), sendCommand, 1); } else if (meta.message.high_temperature_breaker) { const threshold = meta.state.high_temperature_threshold; const buf = Buffer.alloc(12); buf.writeUInt8(5, 8); buf.writeUInt8(utils.getFromLookup(meta.message.high_temperature_breaker, onOffLookup), 9); buf.writeUInt16BE(utils.toNumber(threshold, "high_temperature_threshold"), 10); await sendDataPointRaw(entity, 17, Array.from(buf), sendCommand, 1); } }, from: (v) => { const data = Buffer.from(v, "base64"); const result = {}; const lookup = { 0: "OFF", 1: "ON" }; const alarmLookup = { 3: "overload", 4: "leakage", 5: "high_temperature" }; const len = data.length; let i = 0; while (i < len) { if (Object.hasOwn(alarmLookup, data[i])) { const alarm = alarmLookup[data[i]]; const state = lookup[data[i + 1]]; const threshold = data[i + 3] | (data[i + 2] << 8); result[`${alarm}_breaker`] = state; result[`${alarm}_threshold`] = threshold; } i += 4; } return result; }, }, threshold_3: { to: async (v, meta) => { const entity = meta.device.endpoints[0]; const onOffLookup = { on: 1, off: 0 }; const sendCommand = utils.getMetaValue(entity, meta.mapped, "tuyaSendCommand", undefined, "dataRequest"); if (meta.message.over_current_threshold) { const state = meta.state.over_current_breaker; const buf = Buffer.from([ 1, utils.getFromLookup(state, onOffLookup), 0, utils.toNumber(meta.message.over_current_threshold, "over_current_threshold"), ]); await sendDataPointRaw(entity, 18, Array.from(buf), sendCommand, 1); } else if (meta.message.over_current_breaker) { const threshold = meta.state.over_current_threshold; const buf = Buffer.from([ 1, utils.getFromLookup(meta.message.over_current_breaker, onOffLookup), 0, utils.toNumber(threshold, "over_current_threshold"), ]); await sendDataPointRaw(entity, 18, Array.from(buf), sendCommand, 1); } else if (meta.message.over_voltage_threshold) { const state = meta.state.over_voltage_breaker; const buf = Buffer.alloc(8); buf.writeUInt8(3, 4); buf.writeUInt8(utils.getFromLookup(state, onOffLookup), 5); buf.writeUInt16BE(utils.toNumber(meta.message.over_voltage_threshold, "over_voltage_threshold"), 6); await sendDataPointRaw(entity, 18, Array.from(buf), sendCommand, 1); } else if (meta.message.over_voltage_breaker) { const threshold = meta.state.over_voltage_threshold; const buf = Buffer.alloc(8); buf.writeUInt8(3, 4); buf.writeUInt8(utils.getFromLookup(meta.message.over_voltage_breaker, onOffLookup), 5); buf.writeUInt16BE(utils.toNumber(threshold, "over_voltage_threshold"), 6); await sendDataPointRaw(entity, 18, Array.from(buf), sendCommand, 1); } else if (meta.message.under_voltage_threshold) { const state = meta.state.under_voltage_breaker; const buf = Buffer.alloc(12); buf.writeUInt8(4, 8); buf.writeUInt8(utils.getFromLookup(state, onOffLookup), 9); buf.writeUInt16BE(utils.toNumber(meta.message.under_voltage_threshold, "under_voltage_threshold"), 10); await sendDataPointRaw(entity, 18, Array.from(buf), sendCommand, 1); } else if (meta.message.under_voltage_breaker) { const threshold = meta.state.under_voltage_threshold; const buf = Buffer.alloc(12); buf.writeUInt8(4, 8); buf.writeUInt8(utils.getFromLookup(meta.message.under_voltage_breaker, onOffLookup), 9); buf.writeUInt16BE(utils.toNumber(threshold, "under_voltage_threshold"), 10); await sendDataPointRaw(entity, 18, Array.from(buf), sendCommand, 1); } else if (meta.message.insufficient_balance_threshold) { const state = meta.state.insufficient_balance_breaker; const buf = Buffer.alloc(16); buf.writeUInt8(8, 12); buf.writeUInt8(utils.getFromLookup(state, onOffLookup), 13); buf.writeUInt16BE(utils.toNumber(meta.message.insufficient_balance_threshold, "insufficient_balance_threshold"), 14); await sendDataPointRaw(entity, 18, Array.from(buf), sendCommand, 1); } else if (meta.message.insufficient_balance_breaker) { const threshold = meta.state.insufficient_balance_threshold; const buf = Buffer.alloc(16); buf.writeUInt8(8, 12); buf.writeUInt8(utils.getFromLookup(meta.message.insufficient_balance_breaker, onOffLookup), 13); buf.writeUInt16BE(utils.toNumber(threshold, "insufficient_balance_threshold"), 14); await sendDataPointRaw(entity, 18, Array.from(buf), sendCommand, 1); } }, from: (v) => { const data = Buffer.from(v, "base64"); const result = {}; const lookup = { 0: "OFF", 1: "ON" }; const alarmLookup = { 1: "over_current", 3: "over_voltage", 4: "under_voltage", 8: "insufficient_balance" }; const len = data.length; let i = 0; while (i < len) { if (Object.hasOwn(alarmLookup, data[i])) { const alarm = alarmLookup[data[i]]; const state = lookup[data[i + 1]]; const threshold = data[i + 3] | (data[i + 2] << 8); result[`${alarm}_breaker`] = state; result[`${alarm}_threshold`] = threshold; } i += 4; } return result; }, }, selfTestResult: exports.valueConverterBasic.lookup({ checking: 0, success: 1, failure: 2, others: 3 }), lockUnlock: exports.valueConverterBasic.lookup({ LOCK: true, UNLOCK: false }), localTempCalibration1: { from: (v) => { // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` if (v > 55) v -= 0x100000000; return v / 10; }, to: (v) => { if (v > 0) return v * 10; if (v < 0) return v * 10 + 0x100000000; return v; }, }, localTempCalibration2: { from: (v) => v, to: (v) => { if (v < 0) return v + 0x100000000; return v; }, }, localTempCalibration3: { from: (v) => { // biome-ignore lint/style/noParameterAssign: ignored using `--suppress` if (v > 0x7fffffff) v -= 0x100000000; return v / 10; }, to: (v) => { if (v > 0) return v * 10; if (v < 0) return v * 10 + 0x100000000; return v; }, }, thermostatHolidayStartStop: { from: (v) => { const start = { year: v.slice(0, 4), month: v.slice(4, 6), day: v.slice(6, 8), hours: v.slice(8, 10), minutes: v.slice(10, 12), }; const end = { year: v.slice(12, 16), month: v.slice(16, 18), day: v.slice(18, 20), hours: v.slice(20, 22), minutes: v.slice(22, 24), }; const startStr = `${start.year}/${start.month}/${start.day} ${start.hours}:${start.minutes}`; const endStr = `${end.year}/${end.month}/${end.day} ${end.hours}:${end.minutes}`; return `${startStr} | ${endStr}`; }, to: (v) => { const numberPattern = /\d+/g; // @ts-expect-error ignore return v.match(numberPattern).join([]).toString(); }, }, thermostatScheduleDaySingleDP: { from: (v) => { // day split to 10 min segments = total 144 segments const maxPeriodsInDay = 10; const periodSize = 3; const schedule = []; for (let i = 0; i < maxPeriodsInDay; i++) { const time = v[i * periodSize]; const totalMinutes = time * 10; const hours = totalMinutes / 60; const rHours = Math.floor(hours); const minutes = (hours - rHours) * 60; const rMinutes = Math.round(minutes); const strHours = rHours.toString().padStart(2, "0"); const strMinutes = rMinutes.toString().padStart(2, "0"); const tempHexArray = [v[i * periodSize + 1], v[i * periodSize + 2]]; const tempRaw = Buffer.from(tempHexArray).readUIntBE(0, tempHexArray.length); const temp = tempRaw / 10; schedule.push(`${strHours}:${strMinutes}/${temp}`); if (rHours === 24) break; } return schedule.join(" "); }, to: (v, meta) => { const dayByte = { monday: 1, tuesday: 2, wednesday: 4, thursday: 8, friday: 16, saturday: 32, sunday: 64, }; const weekDay = v.week_day; utils.assertString(weekDay, "week_day"); if (Object.keys(dayByte).indexOf(weekDay) === -1) { throw new Error(`Invalid "week_day" property value: ${weekDay}`); } let weekScheduleType = "separate"; if (meta.state?.working_day) { weekScheduleType = String(meta.state.working_day); } const payload = []; switch (weekScheduleType) { case "mon_sun": payload.push(127); break; case "mon_fri+sat+sun": if (["saturday", "sunday"].indexOf(weekDay) === -1) { payload.push(31); break; } payload.push(dayByte[weekDay]); break; case "separate": payload.push(dayByte[weekDay]); break; default: throw new Error('Invalid "working_day" property, need to set it before'); } // day split to 10 min segments = total 144 segments const maxPeriodsInDay = 10; utils.assertString(v.schedule, "schedule"); const schedule = v.schedule.split(" "); const schedulePeriods = schedule.length; if (schedulePeriods > 10) throw new Error(`There cannot be more than 10 periods in the schedule: ${v}`); if (schedulePeriods < 2) throw new Error(`There cannot be less than 2 periods in the schedule: ${v}`); // biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress` let prevHour; for (const period of schedule) { const timeTemp = period.split("/"); const hm = timeTemp[0].split(":", 2); const h = Number.parseInt(hm[0]); const m = Number.parseInt(hm[1]); const temp = Number.parseFloat(timeTemp[1]); if (h < 0 || h > 24 || m < 0 || m >= 60 || m % 10 !== 0 || temp < 5 || temp > 30 || temp % 0.5 !== 0) { throw new Error(`Invalid hour, minute or temperature of: ${period}`); } if (prevHour > h) { throw new Error(`The hour of the next segment can't be less than the previous one: ${prevHour} > ${h}`); } prevHour = h; const segment = (h * 60 + m) / 10; const tempHexArray = convertDecimalValueTo2ByteHexArray(temp * 10); payload.push(segment, ...tempHexArray); } // Add "technical" periods to be valid payload for (let i = 0; i < maxPeriodsInDay - schedulePeriods; i++) { // by default it sends 9000b2, it's 24 hours and 18 degrees payload.push(144, 0, 180); } return payload; }, }, thermostatScheduleDayMultiDP: { from: (v) => exports.valueConverter.thermostatScheduleDayMultiDPWithTransitionCount().from(v), to: (v) => exports.valueConverter.thermostatScheduleDayMultiDPWithTransitionCount().to(v), }, thermostatScheduleDayMultiDPWithTransitionCount: (transitionCount = 4) => { return { from: (v) => { const schedule = []; for (let index = 1; index < transitionCount * 4 - 1; index = index + 4) { schedule.push( // @ts-expect-error `${String(Number.parseInt(v[index + 0])).padStart(2, "0")}:${String(Number.parseInt(v[index + 1])).padStart(2, "0")}/${(Number.parseFloat((v[index + 2] << 8) + v[index + 3]) / 10.0).toFixed(1)}`); } return schedule.join(" "); }, to: (v) => { const payload = [0]; const transitions = v.split(" "); if (transitions.length !== transitionCount) { throw new Error(`Invalid schedule: there should be ${transitionCount} transitions`); } for (const transition of transitions) { const timeTemp = transition.split("/"); if (timeTemp.length !== 2) { throw new Error(`Invalid schedule: wrong transition format: ${transition}`); } const hourMin = timeTemp[0].split(":"); const hour = Number.parseInt(hourMin[0]); const min = Number.parseInt(hourMin[1]); const temperature = Math.floor(Number.parseFloat(timeTemp[1]) * 10); if (hour < 0 || hour > 24 || min < 0 || min > 60 || temperature < 50 || temperature > 350) { throw new Error(`Invalid hour, minute or temperature of: ${transition}`); } payload.push(hour, min, (t