zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
1,065 lines (1,064 loc) • 177 kB
JavaScript
"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.BacklightColorEnum = exports.enum = exports.Enum = exports.whitelabel = exports.fingerprint = exports.configureBindBasic = exports.configureMcuVersionRequest = exports.configureQuery = exports.configureMagicPacket = exports.skip = exports.exposes = exports.TuyaWeatherID = exports.F3ProTuyaWeatherCondition = exports.M8ProTuyaWeatherCondition = exports.dataTypes = void 0;
exports.convertBufferToNumber = convertBufferToNumber;
exports.convertDecimalValueTo4ByteHexArray = convertDecimalValueTo4ByteHexArray;
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 libColor = __importStar(require("../lib/color"));
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
};
exports.M8ProTuyaWeatherCondition = {
sunny: 100,
heavy_rain: 101,
cloudy: 102,
sandstorm: 103,
light_snow: 104,
snow: 105,
freezing_fog: 106,
rainstorm: 107,
shower: 108,
dust: 109,
spit: 112,
sleet: 113,
yin: 114,
freezing_rain: 115,
rain: 118,
fog: 121,
heavy_shower: 123,
heavy_snow: 124,
heavy_downpour: 125,
blizzard: 126,
hailstone: 127,
snow_shower: 130,
haze: 140,
thunder_shower: 143,
};
exports.F3ProTuyaWeatherCondition = {
heavy_rain: 101,
thunderstorm: 102,
dust_storm: 103,
light_snow: 104,
snow: 105,
freezing_fog: 106,
shower: 108,
floating_dust: 109,
thunder_and_lighting: 110,
light_shower: 111,
rain: 112,
rain_and_snow: 113,
dust_bowl: 114,
ice_pellets: 115,
strong_dust_storms: 116,
sandy: 117,
light_to_moderate_rain: 118,
mostly_sunny: 119,
sunny: 120,
haze: 121,
heavy_shower: 123,
heavy_snow: 124,
very_heavy_rain: 125,
blizzard: 126,
ice_pod: 127,
light_to_moderate_snow: 128,
few_clouds: 129,
light_snow_showers: 130,
moderate_snow: 131,
cloudy: 132,
icy_needles: 133,
thunderstorm_with_ice_pods: 136,
freezing_rain: 137,
snow_shower: 138,
light_rain: 139,
thunder: 140,
moderate_rain: 141,
moderate_to_heavy_rain: 144,
};
var TuyaWeatherID;
(function (TuyaWeatherID) {
TuyaWeatherID[TuyaWeatherID["Temperature"] = 1] = "Temperature";
TuyaWeatherID[TuyaWeatherID["Humidity"] = 2] = "Humidity";
TuyaWeatherID[TuyaWeatherID["Condition"] = 3] = "Condition";
})(TuyaWeatherID || (exports.TuyaWeatherID = TuyaWeatherID = {}));
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 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));
}
// Return `seq` - transaction ID for handling concrete response
async function sendDataPoints(entity, dpValues, cmd = "dataRequest", seq) {
if (seq === undefined) {
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: Buffer.from(convertDecimalValueTo4ByteHexArray(value)) };
}
function dpValueFromBool(dp, value) {
return { dp, datatype: exports.dataTypes.bool, data: Buffer.from([value ? 1 : 0]) };
}
function dpValueFromEnum(dp, value) {
return { dp, datatype: exports.dataTypes.enum, data: Buffer.from([value]) };
}
function dpValueFromString(dp, string) {
return { dp, datatype: exports.dataTypes.string, data: Buffer.from(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: Buffer.from([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"),
countdown_min: () => e
.numeric("countdown", ea.STATE_SET)
.withValueMin(1)
.withValueMax(240)
.withValueStep(1)
.withUnit("min")
.withDescription("Turn off the sprinkler after set duration (one time)"),
on_with_countdown: () => e
.numeric("on_with_countdown", ea.STATE_SET)
.withValueMin(1)
.withValueMax(240)
.withValueStep(1)
.withUnit("min")
.withDescription("Turn on the sprinkler and start countdown"),
countdown_left: () => e
.numeric("countdown_left", ea.STATE)
.withValueMin(0)
.withValueMax(240)
.withValueStep(1)
.withUnit("min")
.withDescription("Time left in the countdown"),
single_watering_duration: () => e.numeric("single_watering_duration", ea.STATE).withDescription("Duration of last watering").withUnit("s"),
flow_switch: () => e
.binary("flow_switch", ea.STATE_SET, "ON", "OFF")
.withDescription("Enables water flow measurement, and automatically turn off the sprinkler when flow is 0 for ~30s"),
quantitative_watering: () => e
.numeric("quantitative_watering", ea.STATE_SET)
.withValueMin(1)
.withValueMax(10000)
.withValueStep(1)
.withUnit("L")
.withDescription("Turn on the sprinkler with a set amount of water"),
single_watering_amount: () => e.numeric("single_watering_amount", ea.STATE).withUnit("L").withDescription("Quantity of last watering"),
surplus_flow: () => e.numeric("surplus_flow", ea.STATE).withUnit("L").withDescription("Remaining amount"),
water_total: () => e.numeric("water_total", ea.STATE).withUnit("L").withValueMin(0).withValueStep(0.001).withDescription("Total watering amount"),
water_current: () => e.numeric("water_current", ea.STATE).withUnit("L/min").withValueMin(0).withValueStep(0.001).withDescription("Current water flow"),
water_total_reset: () => e.enum("water_total_reset", ea.STATE_SET, ["reset"]).withDescription("Reset the stored watering amount to 0").withCategory("config"),
refresh: () => e.enum("refresh", ea.STATE_SET, ["refresh"]).withDescription("Refresh the device status").withCategory("config"),
status_sprinkler: () => e.enum("status", ea.STATE, ["off", "on_auto", "button_locked", "on_manual_app", "on_manual_button"]).withDescription("Status"),
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"),
fault: () => e.binary("fault", ea.STATE, true, false).withDescription("Indicates whether a fault was detected").withCategory("diagnostic"),
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").withCategory("config"),
switchTypeCurtain: () => e
.enum("switch_type_curtain", ea.ALL, ["flip-switch", "sync-switch", "button-switch", "button2-switch"])
.withDescription("External switch type")
.withCategory("config"),
switchTypeButton: () => e.enum("switch_type_button", ea.ALL, ["release", "press"]).withDescription("Determines when the button actuates").withCategory("config"),
backlightModeLowMediumHigh: () => e.enum("backlight_mode", ea.ALL, ["low", "medium", "high"]).withDescription("Intensity of the backlight").withCategory("config"),
backlightModeOffNormalInverted: () => e.enum("backlight_mode", ea.ALL, ["off", "normal", "inverted"]).withDescription("Mode of the backlight").withCategory("config"),
backlightModeOffOn: () => e.binary("backlight_mode", ea.ALL, "ON", "OFF").withDescription("Mode of the backlight").withCategory("config"),
indicatorMode: () => e.enum("indicator_mode", ea.ALL, ["off", "off/on", "on/off", "on"]).withDescription("LED indicator mode").withCategory("config"),
indicatorModeNoneRelayPos: () => e.enum("indicator_mode", ea.ALL, ["none", "relay", "pos"]).withDescription("Mode of the indicator light").withCategory("config"),
powerOutageMemory: () => e.enum("power_outage_memory", ea.ALL, ["on", "off", "restore"]).withDescription("Recover state after power outage").withCategory("config"),
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")
.withCategory("config"),
colorPowerOnBehavior: () => e
.enum("color_power_on_behavior", ea.STATE_SET, ["initial", "previous", "customized"])
.withDescription("Power on behavior state")
.withCategory("config"),
powerOnBehavior: () => e.enum("power_on_behavior", ea.ALL, ["off", "on", "previous"]).withDescription("Power on behavior state").withCategory("config"),
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")
.withCategory("config"),
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")
.withCategory("config"),
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) => {
await utils.ignoreUnsupportedAttribute(async () => {
await device.endpoints[0].read("genBasic", ["manufacturerName", "zclVersion", "appVersion", "modelId", "powerSource", 0xfffe]);
}, "Tuya configureMagicPacket");
};
exports.configureMagicPacket = configureMagicPacket;
const configureQuery = async (device, coordinatorEndpoint) => {
// Required to get the device to start reporting
await device.getEndpoint(1).command("manuSpecificTuya", "dataQuery", {});
};
exports.configureQuery = configureQuery;
const configureMcuVersionRequest = async (device, coordinatorEndpoint) => {
await device.getEndpoint(1).command("manuSpecificTuya", "mcuVersionRequest", { seq: 0x0002 });
};
exports.configureMcuVersionRequest = configureMcuVersionRequest;
const configureBindBasic = async (device, coordinatorEndpoint) => {
await device.getEndpoint(1).bind("genBasic", coordinatorEndpoint);
};
exports.configureBindBasic = configureBindBasic;
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;
exports.BacklightColorEnum = {
red: enumConstructor(0),
blue: enumConstructor(1),
green: enumConstructor(2),
white: enumConstructor(3),
yellow: enumConstructor(4),
magenta: enumConstructor(5),
cyan: enumConstructor(6),
warm_white: enumConstructor(7),
};
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 };
},
multiplyBy: (value) => {
return { to: (v) => v / value, from: (v) => v * value };
},
divideByFromOnly: (value) => {
return { to: (v) => v, from: (v) => v / value };
},
divideByWithLimits: (value, min, max) => {
return {
to: (v) => (v > max ? max * value : v < min ? min * value : v * value),
from: (v) => (v / value > max ? max : v / value < min ? min : 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) }),
switchTypeCurtain: exports.valueConverterBasic.lookup({
"flip-switch": new Enum(0),
"sync-switch": new Enum(1),
"button-switch": new Enum(2),
"button2-switch": new Enum(3),
}),
switchTypeButton: exports.valueConverterBasic.lookup({
release: new Enum(0),
press: new Enum(1),
}),
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),
multiplyBy10: exports.valueConverterBasic.multiplyBy(10),
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(),
fault: { from: (v) => !!v },
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),
},
refresh: {
to: (v) => {
return v === "refresh";
},
from: () => {
return "idle";
},
},
waterConsumption: {
from: (v) => {
const buf = Buffer.isBuffer(v) ? v : Buffer.from(v || []);
if (buf.length >= 8) {
const value = (buf.readUInt8(4) << 24) + (buf.readUInt8(5) << 16) + (buf.readUInt8(6) << 8) + buf.readUInt8(7);
return value / 1000;
}
return 0;
},
},
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, 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, 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, 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, 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, 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, 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),
0,
utils.toNumber(meta.message.over_current_threshold, "over_current_threshold"),
]);
await sendDataPointRaw(entity, 18, 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),
0,
utils.toNumber(threshold, "over_current_threshold"),
]);
await sendDataPointRaw(entity, 18, 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, 0), 5);
buf.writeUInt16BE(utils.toNumber(meta.message.over_voltage_threshold, "over_voltage_threshold"), 6);
await sendDataPointRaw(entity, 18, 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, 0), 5);
buf.writeUInt16BE(utils.toNumber(threshold, "over_voltage_threshold"), 6);
await sendDataPointRaw(entity, 18, 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, 0), 9);
buf.writeUInt16BE(utils.toNumber(meta.message.under_voltage_threshold, "under_voltage_threshold"), 10);
await sendDataPointRaw(entity, 18, 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, 0), 9);
buf.writeUInt16BE(utils.toNumber(threshold, "under_voltage_threshold"), 10);
await sendDataPointRaw(entity, 18, 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, 0), 13);
buf.writeUInt16BE(utils.toNumber(meta.message.insufficient_balance_threshold, "insufficient_balance_threshold"), 14);
await sendDataPointRaw(entity, 18, 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, 0), 13);
buf.writeUInt16BE(utils.toNumber(threshold, "insufficient_balance_threshold"), 14);
await sendDataPointRaw(entity, 18, 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) => {
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) => {
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();
},
},
thermostatHolidayStartStopUnixTS: {
// converts 8-byte big-endian 2 times Unix timestamps array to "YYYY/MM/DD HH:MM | YYYY/MM/DD HH:MM" string
from: (v) => {
if (!v || v.length !== 8)
return "";
// Convert first 4 bytes → start Unix timestamp
const startUnixTS = (v[0] << 24) | (v[1] << 16) | (v[2] << 8) | v[3];
// Convert next 4 bytes → end Unix timestamp
const endUnixTS = (v[4] << 24) | (v[5] << 16) | (v[6] << 8) | v[7];
const fmt = (date) => {
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, "0"); // +1 as JavaScript months are zero-based
const day = String(date.getUTCDate()).padStart(2, "0");
const hours = String(date.getUTCHours()).padStart(2, "0");
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
return `${year}/${month}/${day} ${hours}:${minutes}`;
};
return `${fmt(new Date(startUnixTS * 1000))} | ${fmt(new Date(endUnixTS * 1000))}`;
},
to: (v) => {
// converts from string "YYYY/MM/DD HH:MM | YYYY/MM/DD HH:MM" to 8-byte array
const [startDate, endDate] = v.split("|").map((s) => s.trim());
const parse = (s) => {
const [datePart, timePart] = s.split(" ");
const [y, m, d] = datePart.split("/").map(Number);
const [h, min] = timePart.split(":").map(Number);
const unix = Math.floor(Date.UTC(y, m - 1, d, h, min) / 1000);
return [(unix >> 24) & 0xff, (unix >> 16) & 0xff, (unix >> 8) & 0xff, unix & 0xff];
};
return [...parse(startDate), ...parse(endDate)]; // ... to unpack arrays into elements
},
},
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], 10);
const m = Number.parseInt(hm[1], 10);
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;