zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
4,624 lines • 221 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 = exports.circuitBreakerFaultList = 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.circuitBreakerFaultList = [
"short_circuit",
"surge",
"overload",
"leakage_current",
"temperature",
"fire",
"high_power",
"self_test",
"over_current",
"unbalance",
"over_voltage",
"under_voltage",
"miss_phase",
"outage",
"magnetism", // or negative_power
"credit",
"no_balance",
];
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, signed = true) {
// Input: max 4 bytes in big-endian order
// Output signed: int32 encoded with 2s complement
// Output unsigned: uint32
//
// Examples:
// [ 1, 2, 3, 4] -> [0x01, 0x02, 0x03, 0x04] -> 0x01020304
// -> +16909060 signed / unsigned
// [255, 255, 255, 254] -> [0xFF, 0xFF, 0xFF, 0xFE] -> 0xFFFFFFFE
// -> -2 signed / +4294967294 unsigned
let value = 0;
for (let i = 0; i < chunks.length; i++) {
value = value << 8;
value += chunks[i];
}
if (!signed)
return value >>> 0;
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) {
// Input: int32 or uint32
// Output: 4 bytes in big-endian order
//
// Examples:
// +2 -> 0x00000002 -> [0x00, 0x00, 0x00, 0x02] -> [ 0, 0, 0, 2]
// +4294967294 -> 0XFFFFFFFE -> [0xFF, 0xFF, 0xFF, 0xFE] -> [255, 255, 255, 254]
// -2 -> 0xFFFFFFFE -> [0xFF, 0xFF, 0xFF, 0xFE] -> [255, 255, 255, 254]
// Encode negative values as 2s complement
if (value < 0) {
value = 0x100000000 + 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 = {
alarmTime: () => e
.numeric("alarm_time", ea.STATE_SET)
.withUnit("s")
.withValueMin(1)
.withValueMax(180)
.withValueStep(1)
.withDescription("Alarm time")
.withCategory("config"),
alarmMode: () => e.enum("alarm_mode", ea.STATE_SET, ["arm", "silent", "disarm"]).withDescription("Alarm work mode").withCategory("config"),
alarmStatus: () => e.enum("alarm_status", ea.STATE, ["normal", "alarm"]).withDescription("Indicates when vibration is detected"),
dismissAlarm: () => e.enum("dismiss_alarm", ea.STATE_SET, ["DISMISS"]).withDescription("Stop the buzzer for the current alarm"),
sensitivity: () => e.enum("sensitivity", ea.STATE_SET, ["low", "middle", "high"]).withDescription("Sensitivity level of the sensor").withCategory("config"),
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("Toggle the device after a set duration (one time action)"),
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 action)"),
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"), // current alarm or all alarms?
silentMode: () => e.binary("silent_mode", ea.STATE_SET, "ON", "OFF").withDescription("Mute the buzzer for all alarms").withCategory("config"),
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").withCategory("diagnostic"),
doNotDisturb: () => e
.binary("do_not_disturb", ea.STATE_SET, true, false)
.withDescription("Controls state after power outage: false = on, true = restore previous state")
.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;
},
inchingSwitch2: () => e
.composite("inching", "inching", ea.STATE_SET)
.withDescription("Inching configuration")
.withFeature(e
.binary("state", ea.STATE_SET, "ON", "OFF")
.withDescription("Whenever the device is switched ON, switch it OFF automatically after the configured delay"))
.withFeature(e
.numeric("minutes", ea.STATE_SET)
.withUnit("m")
.withValueMin(0)
.withValueMax(1440)
.withDescription("Minutes component of the delay duration"))
.withFeature(e
.numeric("seconds", ea.STATE_SET)
.withUnit("s")
.withValueMin(0)
.withValueMax(59)
.withDescription("Seconds component of the delay duration"))
.withCategory("config"),
circuitBreakerFaults: () => e
.list("faults", ea.STATE, e.enum("fault", ea.STATE, exports.circuitBreakerFaultList))
.withDescription("List of current faults")
.withCategory("diagnostic"),
circuitBreakerStatus: () => e.enum("status", ea.STATE, ["off", "consumption", "production"]).withDescription("Current operating status").withCategory("diagnostic"),
leakageCurrent: () => e
.numeric("leakage_current", ea.STATE)
.withUnit("mA")
.withDescription("Measured current difference between live and neutral wires")
.withCategory("diagnostic"),
reclosing: () => e
.binary("reclosing", ea.STATE_SET, "ON", "OFF")
.withCategory("config")
.withDescription("Automatically attempt switching ON the circuit after it was turned OFF by a detected fault"),
reclosing_delay: () => e
.numeric("reclosing_delay", ea.STATE_SET)
.withUnit("s")
.withValueMin(1)
.withValueMax(99)
.withValueStep(1)
.withCategory("config")
.withDescription("Time to wait after the fault is cleared, before attempting reclose"),
reclosing_count: () => e
.numeric("reclosing_count", ea.STATE_SET)
.withValueMin(0)
.withValueMax(30)
.withValueStep(1)
.withCategory("config")
.withDescription("Number of allowed reclosing attempts per fault"),
energyPrepayment: () => e
.binary("prepayment", ea.STATE_SET, "ON", "OFF")
.withDescription("Automatically switch OFF the circuit when the energy balance reaches zero")
.withCategory("config"),
energyBalance: () => e
.numeric("energy_balance", ea.STATE)
.withUnit("kWh")
.withDescription("Amount of energy allowed for consumption (Decreases when Prepayment is enabled)")
.withCategory("diagnostic"),
energyBalanceAdd: () => e
.numeric("energy_balance_add", ea.STATE_SET)
.withUnit("kWh")
.withValueMin(0)
.withValueMax(999999)
.withValueStep(0.01)
.withDescription("Add an amount of energy to the balance")
.withCategory("config"),
energyBalanceReset: () => e.enum("energy_balance_reset", ea.STATE_SET, ["RESET"]).withDescription("Set the energy balance to zero").withCategory("config"),
energyReset: () => e.enum("energy_reset", ea.STATE_SET, ["RESET"]).withDescription("Set the energy measurements to zero").withCategory("config"),
leakageCurrentAndTemperatureAlarm: () => e
.composite("alarm_set_1", "alarm_set_1", ea.STATE_SET)
.withDescription("Leakage current and temperature alarms configuration")
.withFeature(e
.binary("leakage_current_alarm", ea.STATE_SET, "ON", "OFF")
.withDescription("Automatically switch OFF the circuit when the leakage current is above the limit (Default ON)"))
.withFeature(e
.numeric("leakage_current_threshold", ea.STATE_SET)
.withUnit("mA")
.withValueMin(1)
.withValueMax(99)
.withValueStep(1)
.withDescription("Leakage current limit (Default 50mA)"))
.withFeature(e
.binary("device_temperature_alarm", ea.STATE_SET, "ON", "OFF")
.withDescription("Automatically switch OFF the circuit when the measured temperature exceeds the limit (Default ON)"))
.withFeature(e
.numeric("device_temperature_threshold", ea.STATE_SET)
.withUnit("°C")
.withValueMin(10)
.withValueMax(85)
.withValueStep(1)
.withDescription("Temperature limit (Default 80°C)"))
.withCategory("config"),
rs485ConfigAndHighPowerAlarm: () => e
.composite("alarm_set_1", "alarm_set_1", ea.STATE_SET)
.withDescription("Configuration for RS485 wired communication (if supported by device) and high power alarm")
.withFeature(e.binary("rs485_baud_rate_enabled", ea.STATE_SET, "ON", "OFF").withLabel("RS485 baud rate"))
.withFeature(e.enum("rs485_baud_rate", ea.STATE_SET, [2400, 4800, 9600, 19200, 38400]).withLabel("RS485 baud rate"))
.withFeature(e.binary("rs485_address_enabled", ea.STATE_SET, "ON", "OFF").withLabel("RS485 address"))
.withFeature(e.numeric("rs485_address", ea.STATE_SET).withValueMin(1).withValueMax(100).withValueStep(1).withLabel("RS485 address"))
.withFeature(e.binary("rs485_data_format_enabled", ea.STATE_SET, "ON", "OFF").withLabel("RS485 data format"))
.withFeature(e.enum("rs485_data_format", ea.STATE_SET, ["N81", "E81", "O81", "N82"]).withLabel("RS485 data format"))
.withFeature(e
.binary("high_power_alarm", ea.STATE_SET, "ON", "OFF")
.withDescription("Trigger alarm when power draw exceeds the limit (Default OFF)"))
.withFeature(e
.numeric("high_power_threshold", ea.STATE_SET)
.withValueMin(0)
.withValueMax(65535)
.withValueStep(1)
.withUnit("kW")
.withDescription("Power limit (Default 25 kW)"))
.withCategory("config"),
currentAndVoltageAlarm: () => e
.composite("alarm_set_2", "alarm_set_2", ea.STATE_SET)
.withDescription("Current and voltage alarms configuration")
.withFeature(e
.binary("over_current_alarm", ea.STATE_SET, "ON", "OFF")
.withDescription("Automatically switch OFF the circuit when the circuit draws more current than the limit (Default ON)"))
.withFeature(e
.numeric("over_current_threshold", ea.STATE_SET)
.withUnit("A")
.withValueMin(1.0)
.withValueMax(80.0)
.withValueStep(0.1)
.withDescription("Current upper limit (Default 63A)"))
.withFeature(e
.binary("over_voltage_alarm", ea.STATE_SET, "ON", "OFF")
.withDescription("Automatically switch OFF the circuit when the voltage is above the limit (Default ON)"))
.withFeature(e
.numeric("over_voltage_threshold", ea.STATE_SET)
.withUnit("V")
.withValueMin(120)
.withValueMax(300)
.withValueStep(1)
.withDescription("Voltage upper limit (Default 275V)"))
.withFeature(e
.binary("under_voltage_alarm", ea.STATE_SET, "ON", "OFF")
.withDescription("Automatically switch OFF the circuit when the voltage is below the limit (Default ON)"))
.withFeature(e
.numeric("under_voltage_threshold", ea.STATE_SET)
.withUnit("V")
.withValueMin(80)
.withValueMax(210)
.withValueStep(1)
.withDescription("Voltage lower limit (Default 175V)"))
.withCategory("config"),
alarmSet2: () => e
.composite("alarm_set_2", "alarm_set_2", ea.STATE_SET)
.withDescription("Configuration for alarms and reporting frequency")
.withFeature(e
.binary("over_current_alarm", ea.STATE_SET, "ON", "OFF")
.withDescription("Trigger alarm when current is higher than the limit (Default OFF)"))
.withFeature(e
.numeric("over_current_threshold", ea.STATE_SET)
.withUnit("A")
.withValueMin(0)
.withValueMax(65535)
.withValueStep(1)
.withDescription("Current upper limit (Default 100A)"))
.withFeature(e
.binary("over_voltage_alarm", ea.STATE_SET, "ON", "OFF")
.withDescription("Trigger alarm when the voltage is above the limit (Default ON)"))
.withFeature(e
.numeric("over_voltage_threshold", ea.STATE_SET)
.withUnit("V")
.withValueMin(0)
.withValueMax(65535)
.withValueStep(1)
.withDescription("Voltage upper limit (Default 253V)"))
.withFeature(e
.binary("under_voltage_alarm", ea.STATE_SET, "ON", "OFF")
.withDescription("Trigger alarm when the voltage is below the limit (Default OFF)"))
.withFeature(e
.numeric("under_voltage_threshold", ea.STATE_SET)
.withUnit("V")
.withValueMin(0)
.withValueMax(65535)
.withValueStep(1)
.withDescription("Voltage lower limit (Default 180V)"))
.withFeature(e
.binary("unbalanced_load_alarm", ea.STATE_SET, "ON", "OFF")
.withDescription("Trigger alarm when power factor is below the limit (Default 15%)"))
.withFeature(e
.numeric("unbalanced_load_threshold", ea.STATE_SET)
.withUnit("%")
.withValueMin(0)
.withValueMax(100)
.withValueStep(1)
.withDescription("Power factor lower limit (Default 15%)"))
.withFeature(e.binary("phase_loss_alarm", ea.STATE_SET, "ON", "OFF").withDescription("Trigger alarm when one phase is unavailable (Default OFF)"))
.withFeature(e
.binary("negative_active_power_alarm", ea.STATE_SET, "ON", "OFF")
.withDescription("Trigger alarm when the active power is negative (Default ON)"))
.withFeature(e.binary("custom_data_reporting_interval", ea.STATE_SET, "ON", "OFF"))
.withFeature(e
.numeric("data_reporting_interval", ea.STATE_SET)
.withUnit("s")
.withValueMin(5)
.withValueMax(600)
.withValueStep(0.5)
.withDescription("How often the device should report measurements (Default 5s)"))
.withCategory("config"),
overCurrentThresholdTime: () => e
.numeric("over_current_threshold_time", ea.STATE_SET)
.withUnit("s")
.withValueMin(0)
.withValueMax(999)
.withValueStep(1)
.withDescription("Time overcurrent is allowed before the circuit is switched OFF (Default 0s)")
.withCategory("config"),
lostFlowAlarm: () => e
.composite("alarm_set_3", "alarm_set_3", ea.STATE_SET)
.withDescription("Lost flow alarm configuration")
.withFeature(e.binary("lost_flow_alarm", ea.STATE_SET, "ON", "OFF").withDescription("Unknown"))
.withFeature(e
.numeric("lost_flow_threshold", ea.STATE_SET)
.withUnit("A")
.withValueMin(1.0)
.withValueMax(100.0)
.withValueStep(0.1)
.withDescription("Unknown"))
.withCategory("config"),
lostFlowThresholdTime: () => e
.numeric("lost_flow_threshold_time", ea.STATE_SET)
.withUnit("s")
.withValueMin(0)
.withValueMax(999)
.withValueStep(1)
.withDescription("Unknown (Default 0s)")
.withCategory("config"),
liquidLevelPercent: () => e.numeric("liquid_level_percent", ea.STATE).withUnit("%").withDescription("Liquid level ratio"),
liquidDepth: () => e.numeric("liquid_depth", ea.STATE).withUnit("m").withDescription("Liquid depth"),
liquidDepthMax: () => e
.numeric("liquid_depth_max", ea.STATE_SET)
.withUnit("m")
.withDescription("Distance from sensor to liquid surface")
.withValueMin(0.1)
.withValueMax(5)
.withValueStep(0.01)
.withCategory("config"),
liquidInstallationHeight: () => e
.numeric("installation_height", ea.STATE_SET)
.withUnit("m")
.withDescription("Distance from sensor to bottom of the tank")
.withValueMin(0.1)
.withValueMax(5)
.withValueStep(0.01)
.withCategory("config"),
liquidMinimalPercent: () => e
.numeric("min_set", ea.STATE_SET)
.withUnit("%")
.withDescription("Liquid minimum percentage")
.withValueMin(0)
.withValueMax(100)
.withValueStep(1)
.withCategory("config"),
liquidMaximalPercent: () => e
.numeric("max_set", ea.STATE_SET)
.withUnit("%")
.withDescription("Liquid maximum percentage")
.withValueMin(0)
.withValueMax(100)
.withValueStep(1)
.withCategory("config"),
liquidState: () => e.enum("liquid_state", ea.STATE, ["low", "normal", "high"]).withDescription("Liquid level status"),
powerSupplyVoltage: () => e.numeric("voltage", ea.STATE).withUnit("V").withDescription("Power supply voltage").withCategory("diagnostic"),
relaySwitch: () => e.binary("relay_switch", ea.STATE_SET, "ON", "OFF").withCategory("config"),
pumpMode: () => e.enum("pump_mode", ea.STATE_SET, ["supply", "drainage"]).withCategory("config"),
autoPumpControl: () => e.enum("pump_control", ea.STATE_SET, ["auto", "manual"]).withCategory("config"),
version: () => e.text("version", ea.STATE).withCategory("diagnostic"),
alarmDuration: () => e.numeric("alarm_duration", ea.STATE_SET).withUnit("min").withValueMin(1).withValueMax(60).withValueStep(1).withCategory("config"),
coverPosition: () => e.cover_position().setAccess("position", ea.STATE_SET),
motorState: () => e
.enum("motor_state", ea.STATE, ["opening", "closing", "stopped"])
.withDescription("Current motor movement status")
.withCategory("diagnostic"),
motorDirection: () => e.enum("motor_direction", ea.STATE_SET, ["normal", "reversed"]).withDescription("Motor rotation direction").withCategory("config"),
motorDirectionSide: () => e.enum("motor_direction", ea.STATE_SET, ["left", "right"]).withDescription("Motor side").withCategory("config"),
slowMode: () => e.binary("slow_mode", ea.STATE_SET, "ON", "OFF").withDescription("Operate the motor slower and quieter than normal").withCategory("config"),
coverType: () => e
.enum("cover_type", ea.STATE_SET, ["roman_pole", "roller_blind", "canopy_curtain", "roman_blind", "honeycomb_curtain"])
.withDescription("Type of window covers installed")
.withCategory("config"),
favoritePosition: () => e
.numeric("favorite_position", ea.STATE_SET)
.withUnit("%")
.withValueMin(0)
.withValueMax(100)
.withValueStep(1)
.withDescription("Store the preferred cover position")
.withCategory("config"),
coverLimit: () => e
.enum("cover_limit", ea.STATE_SET, ["set_up", "set_down", "delete_up", "delete_down", "delete_both"])
.withDescription("Set current position as the limit position")
.withCategory("config"),
clickControl: () => e.enum("click_control", ea.STATE_SET, ["up", "down"]).withDescription("Step control"),
};
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;
function parseThresholds(buffer, definitions) {
const onOffLookup = { 0: "OFF", 1: "ON" };
const result = {};
for (let i = 0; i < buffer.length; i += 4) {
const id = buffer[i];
const definition = definitions[id];
if (!definition) {
continue;
}
result[definition.enabled] = onOffLookup[buffer[i + 1]];
if (definition.value)
result[definition.value] = buffer.readUInt16BE(i + 2);
}
return result;
}
function encodeThresholds(values, currentState, definitions) {
const onOffLookup = { OFF: 0, ON: 1 };
const result = [];
for (const [id, definition] of Object.entries(definitions)) {
const enabled = values[definition.enabled] ?? currentState[definition.enabled];
const value = !definition.value ? 0 : (values[definition.value] ?? currentState[definition.value]);
if (enabled === undefined || value === undefined) {
continue;
}
const buf = Buffer.alloc(4);
buf.writeUInt8(Number(id), 0);
buf.writeUInt8(utils.getFromLookup(enabled, onOffLookup), 1);
buf.writeUInt16BE(Number(value), 2);
result.push(...buf);
}
return result;
}
const alarmSet1ThresholdDefinitions = {
4: {
enabled: "leakage_current_alarm",
value: "leakage_current_threshold",
},
5: {
enabled: "device_temperature_alarm",
value: "device_temperature_threshold",
},
};
const alarmSet1BThresholdDefinitions = {
3: {
enabled: "rs485_baud_rate_enabled",
value: "rs485_baud_rate",
},
4: {
enabled: "rs485_address_enabled",
value: "rs485_address",
},
5: {
enabled: "rs485_data_format_enabled",
value: "rs485_data_format",
},
7: {
enabled: "high_power_alarm",
value: "high_power_threshold",
},
};
const alarmSet2ThresholdDefinitions = {
1: {
enabled: "over_current_alarm",
value: "over_current_threshold",
},
2: {
enabled: "unbalanced_load_alarm",
value: "unbalanced_load_threshold",
},
3: {
enabled: "over_voltage_alarm",
value: "over_voltage_threshold",
},
4: {
enabled: "under_voltage_alarm",
value: "under_voltage_threshold",
},
5: {
enabled: "phase_loss_alarm",
},
7: {
enabled: "negative_active_power_alarm",
},
8: {
enabled: "custom_data_reporting_interval",
value: "data_reporting_interval",
},
9: {
enabled: "device_locating",
value: "device_locating_threshold",
},
};
const alarmSet3ThresholdDefinitions = {
3: {
enabled: "lost_flow_alarm",
value: "lost_flow_threshold",
},
};
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 }),
onOffEnumOn1: exports.valueConverterBasic.lookup({ ON: new Enum(1), OFF: new Enum(0) }),
onOffEnumOn0: exports.valueConverterBasic.lookup({ ON: new Enum(0), OFF: new Enum(1) }),
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 },
level: exports.valueConverterBasic.lookup({ low: new Enum(1), normal: new Enum(0), high: new Enum(2) }),
pumpMode: exports.valueConverterBasic.lookup({ supply: true, drainage: false }),
pumpControl: exports.valueConverterBasic.lookup({ auto: true, manual: false }),
alarmMode: exports.valueConverterBasic.lookup({ arm: new Enum(0), silent: new Enum(1), disarm: new Enum(2) }),
alarmStatus: exports.valueConverterBasic.lookup({ normal: new Enum(0), alarm: new Enum(1) }),
sensitivity: exports.valueConverterBasic.lookup({ low: new Enum(0), middle: new Enum(1), high: new Enum(2) }),
dismiss: {
to: (v) => {
if (v === "DISMISS")
return new Enum(0);
},
from: () => {
return "idle";
},
},
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;
},
},
fertility: exports.valueConverterBasic.lookup({
normal: new Enum(0),
lower: new Enum(1),
low: new Enum(2),
middle: new Enum(3),
high: new Enum(4),
higher: new Enum(5),
}),
temperature_humidity_alarm: exports.valueConverterBasic.lookup({
lower_alarm: new Enum(0),
upper_alarm: new Enum(1),
cancel: new Enum(2),
}),
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;
},
},
coverAction: exports.valueConverterBasic.lookup({ OPEN: new Enum(0), STOP: new Enum(1), CLOSE: new Enum(2), CONTINUE: new Enum(3) }),
motorState: exports.valueConverterBasic.lookup({ opening: new Enum(0), closing: new Enum(1), stopped: new Enum(2) }),
tubularMotorDirection: exports.valueConverterBasic.lookup({ normal: new Enum(0), reversed: new Enum(1) }),
motorDirectionSide: exports.valueConverterBasic.lookup({ left: new Enum(0), right: new Enum(1) }),
coverType: exports.valueConverterBasic.lookup({
roman_pole: new Enum(0),
roller_blind: new Enum(1),
canopy_curtain: new Enum(2),
roman_blind: new Enum(3),
honeycomb_curtain: new Enum(4),
}),
coverLimit: exports.valueConverterBasic.lookup({
set_up: new Enum(0),
set_down: new Enum(1),
delete_up: new Enum(2),
delete_down: new Enum(3),
delete_both: new Enum(4),
}),
clickControl: exports.valueConverterBasic.lookup({ up: new Enum(0), down: 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) => {
// Payload is 8 bytes: voltage (2), current (3), power (3), same layout as
// phaseVariant3/phaseVariant4. Reading only the low 2 bytes of current and
// power made the current wrap above 65.536 A.
//
// Support negative power readings
// https://github.com/Koenkk/zigbee2mqtt/issues/18603#issuecomment-2277697295
// Negative values are not two's complement: they are reported as
// NEGATIVE_POWER_OFFSET + power, so the sign bit cannot be used and the
// branch is taken on an implausibly high reading instead. The 0x999a
// constant previously used here is the low 16 bits of this offset, i.e. an
// artifact of the same truncation.
const NEGATIVE_POWER_OFFSET = 0x19999a;
const IMPLAUSIBLE_POWER = 0x100000; // 1048576 W on a single phase
return {
from: (v) => {
const buf = Buffer.from(v, "base64");
let power = buf[7] | (buf[6] << 8) | (buf[5] << 16);
if (power > IMPLAUSIBLE_POWER) {
power -= NEGATIVE_POWER_OFFSET;
}
return {
[`voltage_${phase}`]: (buf[1] | (buf[0] << 8)) / 10,
[`current_${phase}`]: (buf[4] | (buf[3] << 8) | (buf[2] << 16)) / 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],
};
},
},
phaseVariant4: {
from: (v) => {
return {
voltage: v.readUint16BE(0) / 10,
current: ((v.readUint8(2) << 16) + (v.readUint8(3) << 8) + v.readUint8(4)) / 1000,
power: (v.readUint8(5) << 16) + (v.readUint8(6) << 8) + v.readUint8(7),
};
},
},
phaseVariant5: {
from: (v) => {
const buf = Buffer.isBuffer(v) ? v : Buffer.from(v, "base64");
return {
voltage: ((buf[2] << 8) | buf[3]) / 10,
current: ((buf[5] << 8) | buf[6]) / 1000,
power: (buf[8] << 8) | buf[9],
};
},
},
onOffWithZeros: {
to: (v) => {
if (v === "OFF")
return false;
if (v === "ON")
return true;
},
from: (v, meta, options, publish) => {
let result = "ON";
if (!v) {
// device is slow to report, we can assume zero values when off
publish({ status: "off", current: 0, power: 0 });
result = "OFF";
}
// setting/changing the state resets the countdown, but device doesn't report it
if (meta.state.state !== result)
publish({ countdown: 0 });
return result;
},
},
onOffFingerbot: {
to: (v) => {
if (v === "OFF")
return false;
if (v === "ON")
return true;
},
from: (v, meta, options, publish) => {
publish({ switch_states: "idle" });
if (v)
return "ON";
return "OFF";
},
},
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;
},
},
threshold_4: {
from: (v) => parseThresholds(v, alarmSet1ThresholdDefinitions),
to: (v, meta) => encodeThresholds(v, meta.state.alarm_set_1 || {}, alarmSet1ThresholdDefinitions),
},
threshold_5: {
from: (v) => {
const result = parseThresholds(v, alarmSet2ThresholdDefinitions);
if (result["over_current_threshold"]) {
result["over_current_threshold"] = Number(result["over_current_threshold"]) / 10;
}
return result;
},
to: (v, meta) => {
const vEncoded = { ...v };
if (v["over_current_threshold"]) {
vEncoded["over_current_threshold"] = Number(v["over_current_threshold"] * 10);
}
return encodeThresholds(vEncoded, meta.state.alarm_set_2 || {}, alarmSet2ThresholdDefinitions);
},
},
threshold_6: {
from: (v) => {
const result = parseThresholds(v, alarmSet3ThresholdDefinitions);
if (result["lost_flow_threshold"]) {
result["lost_flow_threshold"] = Number(result["lost_flow_threshold"]) / 10;
}
return result;
},
to: (v, meta) => {
const vEncoded = { ...v };
if (v["lost_flow_threshold"]) {
vEncoded["lost_flow_threshold"] = Number(v["lost_flow_threshold"] * 10);
}
return encodeThresholds(vEncoded, meta.state.alarm_set_3 || {}, alarmSet3ThresholdDefinitions);
},
},
threshold_7: {
from: (v) => {
const result = parseThresholds(v, alarmSet1BThresholdDefinitions);
if (result["rs485_baud_rate"]) {
result["rs485_baud_rate"] = 1200 * 2 ** Number(result["rs485_baud_rate"]);
}
if (result["rs485_data_format"]) {
result["rs485_data_format"] = utils.getFromLookup(result["rs485_data_format"], { 81: "N81", 82: "E81", 83: "O81", 84: "N82" });
}
return result;
},
to: (v, meta) => {
const vEncoded = { ...v };
if (v["rs485_baud_rate"]) {
vEncoded["rs485_baud_rate"] = Math.log2(Number(v["rs485_baud_rate"]) / 1200);
}
if (v["rs485_data_format"]) {
vEncoded["rs485_data_format"] = utils.getFromLookup(v["rs485_data_format"], { N81: 81, E81: 82, O81: 83, N82: 84 });
}
return encodeThresholds(vEncoded, meta.state.alarm_set_1 || {}, alarmSet1BThresholdDefinitions);
},
},
threshold_8: {
from: (v) => {
const result = parseThresholds(v, alarmSet2ThresholdDefinitions);
if (result["data_reporting_interval"]) {
result["data_reporting_interval"] = Number(result["data_reporting_interval"]) / 2;
}
return result;
},
to: (v, meta) => {
const vEncoded = { ...v };
if (v["data_reporting_interval"]) {
vEncoded["data_reporting_interval"] = Number(v["data_reporting_interval"] * 2);
}
return encodeThresholds(vEncoded, meta.state.alarm_set_2 || {}, alarmSet2ThresholdDefinitions);
},
},
selfTestResult: exports.valueConverterBasic.lookup({ checking: 0, success: 1, failure: 2, others: 3 }),
lockUnlock: exports.valueConverterBasic.lookup({ LOCK: true, UNLOCK: false }),
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?.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;
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], 10)).padStart(2, "0")}:${String(Number.parseInt(v[index + 1], 10)).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], 10);
const min = Number.parseInt(hourMin[1], 10);
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, (temperature & 0xff00) >> 8, temperature & 0xff);
}
return payload;
},
};
},
thermostatScheduleDayMultiDPWithDayNumber: (dayNum, transitionCount = 4) => {
return {
from: (v) => exports.valueConverter.thermostatScheduleDayMultiDPWithTransitionCount(transitionCount).from(v),
to: (v) => {
const data = exports.valueConverter.thermostatScheduleDayMultiDPWithTransitionCount(transitionCount).to(v);
data[0] = dayNum;
return data;
},
};
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
thermostatScheduleDayMultiDP_TRV602Z: {
from: (v) => {
const schedule = [];
for (let index = 1; index < 24; index = index + 4) {
const firstByte = (Number.parseInt(v[index + 0], 10) - 192) << 8;
const secondByte = Number.parseInt(v[index + 1], 10);
const minutesSinceMidnight = firstByte | secondByte;
const hour = Math.floor(minutesSinceMidnight / 60);
const minutes = minutesSinceMidnight % 60;
schedule.push(`${String(hour).padStart(2, "0")}:${String(minutes).padStart(2, "0")}/${(Number.parseFloat(v[index + 3]) / 10.0).toFixed(1)}`);
}
return schedule.join(" ");
},
to: (v) => {
const payload = [];
const transitions = v.split(" ");
if (transitions.length !== 6) {
throw new Error("Invalid schedule: there should be 6 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], 10);
const min = Number.parseInt(hourMin[1], 10);
const temperature = Math.floor(Number.parseFloat(timeTemp[1]) * 10);
if (hour < 0 || hour > 24 || min < 0 || min > 60 || temperature < 50 || temperature > 300) {
throw new Error(`Invalid hour, minute or temperature of: ${transition}`);
}
const minutesSinceMidnight = hour * 60 + min;
const firstByte = ((minutesSinceMidnight & 3840) >> 8) + 192;
const secondByte = minutesSinceMidnight & 255;
payload.push(firstByte, secondByte, 64, temperature);
}
return payload;
},
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
thermostatScheduleDayMultiDP_TRV602Z_WithDayNumber: (dayNum) => {
return {
from: (v) => exports.valueConverter.thermostatScheduleDayMultiDP_TRV602Z.from(v),
to: (v) => {
const data = exports.valueConverter.thermostatScheduleDayMultiDP_TRV602Z.to(v);
data.unshift(dayNum);
return data;
},
};
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
thermostatScheduleDayMultiDP_TRV603WZ: {
// Custom schedule value converter for TRV603-WZ 8 pair schedule per day
// Structure:
// [0] metadata (unknown purpose, preserved)
// [1] count = number of subsequent bytes (must be even: pairs*2)
// [2..] alternating (timeByte, thermByte) pairs.
// Encoding rules:
// timeByte = hour * 10 + (minute / 10) (minute must be multiple of 10)
// thermByte = 2 * temperatureC (temperature in 0.5°C steps)
// Decoded textual schedule format:
// "HH:MM/TT.T HH:MM/TT.T HH:MM/TT.T HH:MM/TT.T HH:MM/TT.T HH:MM/TT.T HH:MM/TT.T HH:MM/TT.T"
from: (v) => {
if (!v || v.length < 4)
return "";
// const meta = v[0]; // (always 7?)
const count = v[1];
const segments = [];
for (let i = 0; i < count; i += 2) {
const timeByte = v[2 + i];
const thermByte = v[2 + i + 1];
const hour = Math.floor(timeByte / 10);
const minutes = (timeByte % 10) * 10;
const thermC = thermByte / 2;
segments.push(`${String(hour).padStart(2, "0")}:${String(minutes).padStart(2, "0")}/${thermC.toFixed(1)}`);
}
return segments.join(" ");
},
to: (v) => {
const parts = v.split(/\s+/).filter(Boolean);
const payload = [];
const meta = 7; // keep observed metadata value;
payload.push(meta); // index 0
payload.push(parts.length * 2); // index 1: count of subsequent bytes
for (const segment of parts) {
// Segment format: HH:MM/TT.T (e.g. 06:30/21.0 or 17:30/21.5)
const match = segment.match(/^(\d{2}):(\d{2})\/(\d{1,2}(?:\.5|\.0)?)$/);
if (!match)
throw new Error(`Invalid schedule segment "${segment}", expected format "HH:MM/TT.T"`);
const hour = Number(match[1]);
if (hour < 0 || hour >= 24)
throw new Error(`Invalid hour "${hour}" in schedule segment "${segment}", must be 0-23`);
const minute = Number(match[2]);
if (minute < 0 || minute > 59)
throw new Error(`Invalid minute "${minute}" in schedule segment "${segment}", must be 0-59`);
const thermC = Number(match[3]);
const timeByte = hour * 10 + minute / 10;
const thermByte = Math.round(thermC * 2);
payload.push(timeByte, thermByte);
}
return payload;
},
},
thermostatSchedule: {
from: (value) => {
const buffer = Buffer.from(value, "base64");
const schedules = [];
const scheduleLength = 12;
for (let offset = 0; offset < buffer.length; offset += scheduleLength) {
const b = buffer.slice(offset, offset + scheduleLength);
// temperature
const raw = b.readUInt16BE(3);
const temperatureF = (raw - 0x8000) / 10;
// time in minutes from midnight
const startMinutes = b.readUInt16BE(5);
const endMinutes = b.readUInt16BE(7);
function minutesToTime(m) {
return {
hour: Math.floor(m / 60),
minute: m % 60,
};
}
const daysMask = b[9];
schedules.push({
enabled: (b[1] & 0x80) !== 0,
work_mode: b[2] === 0x02 ? "cooling" : "heating",
temperature_f: temperatureF,
start: minutesToTime(startMinutes),
end: minutesToTime(endMinutes),
week_days: {
sunday: !!(daysMask & 0x01),
monday: !!(daysMask & 0x02),
tuesday: !!(daysMask & 0x04),
wednesday: !!(daysMask & 0x08),
thursday: !!(daysMask & 0x10),
friday: !!(daysMask & 0x20),
saturday: !!(daysMask & 0x40),
},
});
}
return schedules;
},
to: (schedules) => {
const scheduleLength = 12;
const buffers = [];
for (const schedule of schedules) {
const b = Buffer.alloc(scheduleLength, 0x00);
if (schedule.enabled) {
b[1] |= 0x80;
}
b[2] = schedule.work_mode === "cooling" ? 0x02 : 0x00;
const temperatureF = schedule.temperature_f;
const rawTemperature = Math.round(temperatureF * 10) + 0x8000;
b.writeUInt16BE(rawTemperature & 0xffff, 3);
const startMinutes = schedule.start.hour * 60 + schedule.start.minute;
b.writeUInt16BE(startMinutes, 5);
const endMinutes = schedule.end.hour * 60 + schedule.end.minute;
b.writeUInt16BE(endMinutes, 7);
let daysMask = 0;
if (schedule.week_days.sunday)
daysMask |= 0x01;
if (schedule.week_days.monday)
daysMask |= 0x02;
if (schedule.week_days.tuesday)
daysMask |= 0x04;
if (schedule.week_days.wednesday)
daysMask |= 0x08;
if (schedule.week_days.thursday)
daysMask |= 0x10;
if (schedule.week_days.friday)
daysMask |= 0x20;
if (schedule.week_days.saturday)
daysMask |= 0x40;
b[9] = daysMask;
b[10] = 0x02;
let sum = 0;
for (let i = 0; i <= 9; i++) {
sum += b[i];
}
b[11] = sum & 0xff;
buffers.push(b);
}
return Buffer.concat(buffers).toString("base64");
},
},
tv02Preset: () => {
return {
from: (v) => {
if (v === 0)
return "auto";
if (v === 1)
return "manual";
return "holiday"; // 2 and 3 are holiday
},
to: (v, meta) => {
if (v === "auto")
return new Enum(0);
if (v === "manual")
return new Enum(1);
if (v === "holiday") {
// https://github.com/Koenkk/zigbee2mqtt/issues/20486
if (meta.device.manufacturerName === "_TZE200_mudxchsu")
return new Enum(2);
return new Enum(3);
}
throw new Error(`Unsupported preset '${v}'`);
},
};
},
/** @deprecated left for compatibility, use {@link thermostatSystemModeAndPresetMap} */
thermostatSystemModeAndPreset: (toKey) => {
return {
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] };
},
to: (v) => {
const presetLookup = { auto: new Enum(0), manual: new Enum(1), off: new Enum(2), on: new Enum(3) };
const systemModeLookup = { auto: new Enum(1), off: new Enum(2), heat: new Enum(3) };
const lookup = toKey === "preset" ? presetLookup : systemModeLookup;
return utils.getFromLookup(v, lookup);
},
};
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
ZWT198_schedule: {
from: (value, meta, options) => {
const programmingMode = [];
for (let i = 0; i < 8; i++) {
const start = i * 4;
const time = `${value[start].toString().padStart(2, "0")}:${value[start + 1].toString().padStart(2, "0")}`;
const temp = (value[start + 2] * 256 + value[start + 3]) / 10;
const tempStr = `${temp.toFixed(1)}°C`;
programmingMode.push(`${time}/${tempStr}`);
}
return {
schedule_weekday: programmingMode.slice(0, 6).join(" "),
schedule_holiday: programmingMode.slice(6, 8).join(" "),
};
},
to: async (v, meta) => {
const dpId = 109;
const payload = [];
let weekdayFormat;
let holidayFormat;
if (meta.message.schedule_weekday != null) {
weekdayFormat = v;
holidayFormat = meta.state.schedule_holiday;
}
else {
weekdayFormat = meta.state.schedule_weekday;
holidayFormat = v;
}
function scheduleToRaw(key, input, number, payload, meta) {
const items = input.trim().split(/\s+/);
if (items.length !== number) {
throw new Error(`Wrong number of items for ${key} :${items.length}`);
}
for (let i = 0; i < number; i++) {
const timeTemperature = items[i].split("/");
if (timeTemperature.length !== 2) {
throw new Error(`Invalid schedule: wrong transition format: ${items[i]}`);
}
const hourMinute = timeTemperature[0].split(":", 2);
const hour = Number.parseInt(hourMinute[0], 10);
const minute = Number.parseInt(hourMinute[1], 10);
const temperature = Number.parseFloat(timeTemperature[1]);
if (!utils.isNumber(hour) ||
!utils.isNumber(temperature) ||
!utils.isNumber(minute) ||
hour < 0 ||
hour >= 24 ||
minute < 0 ||
minute >= 60 ||
temperature < 5 ||
temperature >= 35) {
throw new Error(`Invalid hour, minute or temperature (5<t<35) in ${key} of: \`${items[i]}\`; Format is \`hh:m/cc.c\` or \`hh:mm/cc.c°C\``);
}
const temperature10 = Math.round(temperature * 10);
payload.push(hour, minute, (temperature10 >> 8) & 0xff, temperature10 & 0xff);
}
return;
}
scheduleToRaw("schedule_weekday", weekdayFormat, 6, payload, meta);
scheduleToRaw("schedule_holiday", holidayFormat, 2, payload, meta);
const entity = meta.device.endpoints[0];
const sendCommand = utils.getMetaValue(entity, meta.mapped, "tuyaSendCommand", undefined, "dataRequest");
await sendDataPointRaw(entity, dpId, Buffer.from(payload), sendCommand, 1);
},
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
PO_BOCO_ELEC_schedule: (day) => ({
to: (v) => {
const payload = [80 + day];
const modeMapping = {
off: 5,
antifrost: 4,
eco: 3,
"comfort_-2": 2,
"comfort_-1": 1,
comfort: 0,
};
const items = v.split(" / ");
items.forEach((item) => {
if (Object.keys(modeMapping).includes(item)) {
payload.push(modeMapping[item]);
}
else {
const time = item.split(":");
const hours = Number.parseInt(time[0], 10);
const minutes = Math.floor(Number.parseInt(time[1], 10) / 5);
const total = hours * 12 + minutes;
if (total > 0)
payload.push(total);
}
});
return payload;
},
from: (v) => {
const payload = [];
const modeMapping = {
5: "off",
4: "antifrost",
3: "eco",
2: "comfort_-2",
1: "comfort_-1",
0: "comfort",
};
for (let index = 1; index < v.length; index++) {
const item = v[index];
if (index % 2) {
if (item > 5)
break;
const mode = modeMapping[item];
payload.push(mode);
}
else {
const nextItem = v[index + 1];
if (nextItem > 5)
break;
const date = new Date();
date.setHours(0, item * 5);
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
payload.push(`${hours}:${minutes}`);
}
}
return payload.join(" / ");
},
}),
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
PO_BOCO_ELEC_holiday: {
to: (v) => {
const payload = [];
const regex = /(?<startYear>\d{4})\/(?<startMonth>\d{2})\/(?<startDay>\d{2})\s(?<startHours>\d{2}):(?<startMinutes>\d{2}) \| (?<endYear>\d{4})\/(?<endMonth>\d{2})\/(?<endDay>\d{2})\s(?<endHours>\d{2}):(?<endMinutes>\d{2}) \| (?<mode>off|antifrost|eco|comfort_-2|comfort_-1|comfort)$/g;
const regexResult = regex.exec(v);
if (!regexResult)
throw new Error("Invalid syntax. Should be" +
"`startYear/startMonth/startDay startHours:startMinutes | endYear/endMonth/endDay endHours:endMinutes | mode`. " +
"For example: `2024/12/12 09:00 | 2024/12/14 10:00 | comfort`");
const { startYear, startMonth, startDay, startHours, startMinutes, endYear, endMonth, endDay, endHours, endMin, mode } = regexResult.groups;
const startDate = new Date(Number.parseInt(startYear, 10), Number.parseInt(startMonth, 10) - 1, Number.parseInt(startDay, 10), Number.parseInt(startHours, 10), Number.parseInt(startMinutes, 10));
const endDate = new Date(Number.parseInt(endYear, 10), Number.parseInt(endMonth, 10) - 1, Number.parseInt(endDay, 10), Number.parseInt(endHours, 10), Number.parseInt(startMinutes, 10));
const diffHours = Math.abs(startDate.getTime() - endDate.getTime()) / 36e5;
const modeMapping = {
off: 5,
antifrost: 4,
eco: 3,
"comfort_-2": 2,
"comfort_-1": 1,
comfort: 0,
};
if (endMin)
logger_1.logger.warning("The end date minutes will be ignore.", NS);
if (diffHours > 255)
throw new Error("You cannot set an interval superior at 255 hours.");
if (startDate.getTime() > endDate.getTime())
throw new Error("You cannot set a negative interval.");
payload.push(Number.parseInt(startYear.slice(2), 10), Number.parseInt(startMonth, 10), Number.parseInt(startDay, 10), Number.parseInt(startHours, 10), Number.parseInt(startMinutes, 10), modeMapping[mode], 0, diffHours);
return payload;
},
from: (v) => {
const startYear = `2${v[0].toString().padStart(3, "0")}`;
const startMonth = v[1].toString().padStart(2, "0");
const startDay = v[2].toString().padStart(2, "0");
const startHours = v[3].toString().padStart(2, "0");
const startMinutes = v[4].toString().padStart(2, "0");
const mode = v[5];
const diffHours = v[7];
const modeMapping = {
5: "off",
4: "antifrost",
3: "eco",
2: "comfort_-2",
1: "comfort_-1",
0: "comfort",
};
const endDate = new Date(Number.parseInt(startYear, 10), Number.parseInt(startMonth, 10) - 1, Number.parseInt(startDay, 10), Number.parseInt(startHours, 10), Number.parseInt(startMinutes, 10));
endDate.setHours(endDate.getHours() + diffHours);
const endYear = endDate.getFullYear().toString();
const endMonth = (endDate.getMonth() + 1).toString().padStart(2, "0");
const endDay = endDate.getDate().toString().padStart(2, "0");
const endHours = endDate.getHours().toString().padStart(2, "0");
return (`${startYear}/${startMonth}/${startDay} ${startHours}:${startMinutes}` +
` | ${endYear}/${endMonth}/${endDay} ${endHours}:${startMinutes}` +
` | ${modeMapping[mode]}`);
},
},
TV02SystemMode: {
to: async (v, meta) => {
const entity = meta.device.endpoints[0];
if (meta.message.system_mode) {
if (meta.message.system_mode === "off") {
await sendDataPointBool(entity, 107, true, "dataRequest", 1);
}
else {
await sendDataPointEnum(entity, 2, 1, "dataRequest", 1); // manual
}
}
else if (meta.message.heating_stop) {
if (meta.message.heating_stop === "ON") {
await sendDataPointBool(entity, 107, true, "dataRequest", 1);
}
else {
await sendDataPointEnum(entity, 2, 1, "dataRequest", 1); // manual
}
}
},
from: (v) => {
return { system_mode: v === false ? "heat" : "off", heating_stop: v === false ? "OFF" : "ON" };
},
},
TV02FrostProtection: {
to: async (v, meta) => {
const entity = meta.device.endpoints[0];
if (v === "ON") {
await sendDataPointBool(entity, 10, true, "dataRequest", 1);
}
else {
await sendDataPointEnum(entity, 2, 1, "dataRequest", 1); // manual
}
},
from: (v) => {
return { frost_protection: v === false ? "OFF" : "ON" };
},
},
inverse: { to: (v) => !v, from: (v) => !v },
onOffNotStrict: { from: (v) => (v ? "ON" : "OFF"), to: (v) => v === "ON" },
errorOrBatteryLow: {
from: (v, meta, options, publish) => {
let batteryLow = false;
if (v === 1)
batteryLow = true;
publish({ error: v });
return batteryLow;
},
},
// https://developer.tuya.com/en/docs/connect-subdevices-to-gateways/tuya-zigbee-multiple-switch-access-standard?id=K9ik6zvnqr09m
inchingSwitch: {
to: (value) => {
let result = "";
for (let i = 1; i <= 6; i++) {
if (value[`inching_control_${i}`] === undefined || value[`inching_time_${i}`] === undefined)
continue;
let state = value[`inching_control_${i}`] === "ENABLE" ? 1 : 0;
if (i !== 1) {
// Second endpoint onwards base number is determined by 2 powered by endpoint number less 1
state += 2 ** (i - 1);
}
const secs = Number.parseInt(value[`inching_time_${i}`], 10);
const byte1 = secs >> 8; // Equivalent to Math.truc(secs / 256)
const byte2 = secs % 256;
const ascii = String.fromCharCode(state, byte1, byte2);
result += Buffer.from(ascii).toString("base64");
}
return result;
},
from: (value) => {
// break the value into 4 char encoded char which will give 3 char when decoded
const data = {};
for (let i = 0; i < value.length; i += 4) {
const b64asc = value.substring(i, i + 4);
const str = Buffer.from(b64asc, "base64").toString("utf8");
const cca0 = str.charCodeAt(0);
const cca1 = str.charCodeAt(1);
const cca2 = str.charCodeAt(2);
let tmp = 0;
let status = "";
// first value indicates the endpoint and if it is on or off
// 0-1 - 1st endpoint, 2-3 - 2nd endpoint, ...
switch (cca0) {
case 0:
data.inching_control_1 = "DISABLE";
data.inching_time_1 = (cca1 << 8) + cca2;
break;
case 1:
data.inching_control_1 = "ENABLE";
data.inching_time_1 = (cca1 << 8) + cca2;
break;
default:
// endpoint #
tmp = Math.trunc(Math.log2(cca0)) + 1;
status = cca0 % 2 ? "ENABLE" : "DISABLE";
data[`inching_control_${tmp}`] = status;
data[`inching_time_${tmp}`] = (cca1 << 8) + cca2;
}
}
return data;
},
},
/** @param toMap the key is 'system_mode' or 'preset' related value */
thermostatSystemModeAndPresetMap: ({ fromMap = {}, toMap = {}, }) => {
return {
from: (v) => {
utils.assertNumber(v, "system_mode");
return { running_mode: fromMap[v].deviceMode, system_mode: fromMap[v].systemMode, preset: fromMap[v].preset };
},
to: (v) => {
return utils.getFromLookup(v, toMap);
},
};
},
utf16BEHexString: {
// String -> hex (UTF-16BE)
to: (v) => {
const s = v.trim();
return Buffer.from(s, "utf16le").swap16().toString("hex");
},
// hex (UTF-16BE) -> String
from: (hex) => {
if (!hex)
return "";
const s = hex.trim();
if ((s.length & 1) !== 0)
return "";
return Buffer.from(s, "hex").swap16().toString("utf16le").trim();
},
},
inchingSwitch2: {
to: (value, meta) => {
const currentState = meta.state.inching || { state: "OFF", minutes: 1, seconds: 0 };
const state = value.state !== undefined ? value.state : currentState.state;
const minutes = value.minutes !== undefined ? value.minutes : currentState.minutes;
const seconds = value.seconds !== undefined ? value.seconds : currentState.seconds;
let totalSeconds = Math.max(1, minutes * 60 + seconds);
if (totalSeconds > 65535)
totalSeconds = 65535;
const buf = Buffer.alloc(3);
buf.writeUInt8(state === "ON" ? 1 : 0, 0);
buf.writeUInt16BE(totalSeconds, 1);
return buf.toString("base64");
},
from: (value) => {
const buf = typeof value === "string" ? Buffer.from(value, "base64") : Buffer.from(value);
const state = buf.readUInt8(0) === 1 ? "ON" : "OFF";
const totalSeconds = buf.readUInt16BE(1);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return { state, minutes, seconds };
},
},
circuitBreakerFaults: {
from: (value) => {
// value is a bitmap where each bit represents a fault flag
const faults = [];
for (let i = 0; i < exports.circuitBreakerFaultList.length; i++) {
const bit = 1 << i;
if ((value & bit) === bit) {
faults.push(exports.circuitBreakerFaultList[i]);
}
}
return faults;
},
},
circuitBreakerFaults1: {
from: (value) => {
// value is a bitmap where each bit represents a fault flag
const faults = [];
for (let i = 0; i < exports.circuitBreakerFaultList.length; i++) {
const bit = 1 << i;
if ((value & bit) === bit) {
if (exports.circuitBreakerFaultList[i] === "magnetism")
faults.push("negative_power");
else
faults.push(exports.circuitBreakerFaultList[i]);
}
}
return faults;
},
},
reset: {
to: (v) => {
if (v === "RESET")
return false;
},
from: (v) => {
return "idle";
},
},
energyBalanceAdd: {
to: (v) => {
return v * 100;
},
from: (v) => {
return 0;
},
},
circuitBreakerStatus: exports.valueConverterBasic.lookup({ off: new Enum(0), consumption: new Enum(1), production: new Enum(2) }), // only 0-1 is confirmed
cycleSchedule: {
// https://developer.tuya.com/en/docs/iot/cycle_timing?id=Kb4vhzu0ryfwm
// Configure device with schedules of alternating on/off cycles.
// Between start and end (time of day), on selected days, repeat:
// ON for onDuration, then OFF for offDuration.
from: (v) => {
const buf = Buffer.from(v, "base64");
const schedules = [];
for (let offset = 10; offset + 9 < buf.length; offset += 10) {
schedules.push({
enabled: buf.readUInt8(offset) === 1,
daysMask: buf.readUInt8(offset + 1),
startTime: buf.readUInt16BE(offset + 2),
endTime: buf.readUInt16BE(offset + 4),
onDuration: buf.readUInt16BE(offset + 6),
offDuration: buf.readUInt16BE(offset + 8),
// unit is minutes
});
}
return schedules;
},
},
GX03ValveState: (zoneNum) => {
const lookup = { 0: "Manual", 1: "Auto", 2: "Closed" };
return {
from: (value, meta, options, publish) => {
// Set initial value to both timers to unlock UI
if (meta.state?.timer_1 === undefined) {
publish({
timer_1: 5,
timer_2: 5,
});
const endpoint = meta.device.getEndpoint(1);
void (async () => {
await sendDataPointValue(endpoint, 13, 5);
await sendDataPointValue(endpoint, 14, 5);
})();
}
// Reset the related countdown on valve closing
if (value === 2) {
publish({ [`countdown_${zoneNum}`]: 0 });
}
if (typeof value === "number" && value in lookup) {
return lookup[value];
}
return `Unknown (${value})`;
},
};
},
autoAdjustment: {
to: (v) => {
return v === "START";
},
from: (v) => {
return "idle";
},
},
switchStates: {
to: (v, meta) => {
if (v === "SWITCH") {
switch (meta.state.state) {
case "ON":
return false;
case "OFF":
return true;
}
}
},
from: (v) => {
return "idle";
},
},
};
const tuyaTz = {
power_on_behavior_1: {
key: ["power_on_behavior", "power_outage_memory"],
convertSet: async (entity, key, value, meta) => {
// Deprecated: remove power_outage_memory
const moesStartUpOnOff = utils.getFromLookup(value, key === "power_on_behavior" ? { off: 0, on: 1, previous: 2 } : { off: 0, on: 1, restore: 2 });
await entity.write("genOnOff", { moesStartUpOnOff });
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("genOnOff", ["moesStartUpOnOff"]);
},
},
power_on_behavior_2: {
key: ["power_on_behavior"],
convertSet: async (entity, key, value, meta) => {
const powerOnBehavior = utils.getFromLookup(value, { off: 0, on: 1, previous: 2 });
await entity.write("manuSpecificTuya3", { powerOnBehavior });
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificTuya3", ["powerOnBehavior"]);
},
},
switch_type: {
key: ["switch_type"],
convertSet: async (entity, key, value, meta) => {
const switchType = utils.getFromLookup(value, { toggle: 0, state: 1, momentary: 2 });
await entity.write("manuSpecificTuya3", { switchType }, { disableDefaultResponse: true });
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificTuya3", ["switchType"]);
},
},
switch_type_curtain: {
key: ["switch_type_curtain"],
convertSet: async (entity, key, value, meta) => {
const switchType = utils.getFromLookup(value, { "flip-switch": 0, "sync-switch": 1, "button-switch": 2, "button2-switch": 3 });
await entity.write("manuSpecificTuya3", { switchType }, { disableDefaultResponse: true });
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificTuya3", ["switchType"]);
},
},
switch_type_button: {
key: ["switch_type_button"],
convertSet: async (entity, key, value, meta) => {
const switchType = utils.getFromLookup(value, { release: 0, press: 1 });
await entity.write("manuSpecificTuya3", { switchType }, { disableDefaultResponse: true });
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificTuya3", ["switchType"]);
},
},
backlight_indicator_mode_1: {
key: ["backlight_mode", "indicator_mode"],
convertSet: async (entity, key, value, meta) => {
const tuyaBacklightMode = utils.getFromLookup(value, key === "backlight_mode" ? { low: 0, medium: 1, high: 2, off: 0, normal: 1, inverted: 2 } : { off: 0, "off/on": 1, "on/off": 2, on: 3 });
await entity.write("genOnOff", { tuyaBacklightMode });
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("genOnOff", ["tuyaBacklightMode"]);
},
},
backlight_indicator_mode_2: {
key: ["backlight_mode"],
convertSet: async (entity, key, value, meta) => {
const tuyaBacklightSwitch = utils.getFromLookup(value, { off: 0, on: 1 });
await entity.write("genOnOff", { tuyaBacklightSwitch });
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("genOnOff", ["tuyaBacklightSwitch"]);
},
},
backlight_indicator_mode_none_relay_pos: {
// In this mode, backlight and indicator are saperately controlled so we need two keys.
// We use "tuyaBacklightSwitch" for backlight's on/off control and "tuyaBacklightMode" for indicator's none/relay/pos control.
key: ["backlight_mode", "indicator_mode"],
convertSet: async (entity, key, value, meta) => {
const lookup = key === "backlight_mode" ? { off: 0, on: 1 } : { none: 0, relay: 1, pos: 2 };
const result = utils.getFromLookup(value, lookup);
if (key === "backlight_mode") {
await entity.write("genOnOff", { tuyaBacklightSwitch: result });
}
else {
await entity.write("genOnOff", { tuyaBacklightMode: result });
}
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
const attribute = key === "backlight_mode" ? "tuyaBacklightSwitch" : "tuyaBacklightMode";
await entity.read("genOnOff", [attribute]);
},
},
child_lock: {
key: ["child_lock"],
convertSet: async (entity, key, value, meta) => {
const v = utils.getFromLookup(value, { lock: true, unlock: false });
await entity.write("genOnOff", { 32768: { value: v, type: 0x10 } });
},
},
min_brightness_attribute: {
key: ["min_brightness"],
convertSet: async (entity, key, value, meta) => {
const number = utils.toNumber(value, "min_brightness");
const minValueHex = number.toString(16);
const maxValueHex = "ff";
const minMaxValue = Number.parseInt(`${minValueHex}${maxValueHex}`, 16);
const payload = { 64512: { value: minMaxValue, type: 0x21 } };
await entity.write("genLevelCtrl", payload, { disableDefaultResponse: true });
return { state: { min_brightness: number } };
},
convertGet: async (entity, key, meta) => {
await entity.read("genLevelCtrl", [0xfc00]);
},
},
min_brightness_command: {
key: ["min_brightness"],
convertSet: async (entity, key, value, meta) => {
utils.assertNumber(value, key);
const payload = { minimum: value };
await entity.command("lightingColorCtrl", "tuyaSetMinimumBrightness", payload);
return { state: { min_brightness: value } };
},
// The response contains the value but as the data type, randomly
// causing malformed messages
// convertGet: async (entity, key, meta) => {
// await entity.read('lightingColorCtrl', [0xf102]);
// },
},
color_power_on_behavior: {
key: ["color_power_on_behavior"],
convertSet: async (entity, key, value, meta) => {
const v = utils.getFromLookup(value, { initial: 0, previous: 1, customized: 2 });
await entity.command("lightingColorCtrl", "tuyaOnStartUp", {
mode: v * 256,
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
});
return { state: { color_power_on_behavior: value } };
},
},
datapoints: {
convertSet: async (entity, key, value, meta) => {
// A set converter is only called once; therefore we need to loop
const state = {};
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
const datapoints = meta.mapped.meta?.tuyaDatapoints;
if (!datapoints)
throw new Error("No datapoints map defined");
for (const [attr, value] of Object.entries(meta.message)) {
const convertedKey = meta.mapped.meta.multiEndpoint && meta.endpoint_name && !attr.startsWith(`${key}_`) ? `${attr}_${meta.endpoint_name}` : attr;
const dpEntry = datapoints.find((d) => d[1] === convertedKey);
if (!dpEntry?.[1] || !dpEntry?.[2].to) {
throw new Error(`No datapoint defined for '${attr}'`);
}
if (dpEntry[3]?.skip?.(meta))
continue;
const dpId = dpEntry[0];
const convertedValue = await dpEntry[2].to(value, meta);
const sendCommand = utils.getMetaValue(entity, meta.mapped, "tuyaSendCommand", undefined, "dataRequest");
if (convertedValue === undefined) {
// conversion done inside converter, ignore.
}
else if (typeof convertedValue === "boolean") {
await sendDataPointBool(entity, dpId, convertedValue, sendCommand, 1);
}
else if (typeof convertedValue === "number") {
await sendDataPointValue(entity, dpId, convertedValue, sendCommand, 1);
}
else if (typeof convertedValue === "string") {
await sendDataPointStringBuffer(entity, dpId, convertedValue, sendCommand, 1);
}
else if (Array.isArray(convertedValue)) {
await sendDataPointRaw(entity, dpId, Buffer.from(convertedValue), sendCommand, 1);
}
else if (convertedValue instanceof Enum) {
await sendDataPointEnum(entity, dpId, convertedValue.valueOf(), sendCommand, 1);
}
else if (convertedValue instanceof Bitmap) {
await sendDataPointBitmap(entity, dpId, convertedValue.valueOf(), sendCommand, 1);
}
else {
throw new Error(`Don't know how to send type '${typeof convertedValue}'`);
}
if (dpEntry[3] && dpEntry[3].optimistic === false)
continue;
state[attr] = value;
}
return { state };
},
},
do_not_disturb: {
key: ["do_not_disturb"],
convertSet: async (entity, key, value, meta) => {
await entity.command("lightingColorCtrl", "tuyaDoNotDisturb", {
enable: value ? 1 : 0,
});
return { state: { do_not_disturb: value } };
},
},
on_off_countdown: {
// Note: This is the Tuya on-off countdown feature documented for switches and smart plugs
// using the Zigbee 'onWithTimedOff' command in a non-standard way.
// There is also an alternative on-off countdown implementation mostly for for Tuya Lighting
// products that uses private commands and attributes. However, those devices should also
// provide datapoints so there is little reason to provide support.
key: ["state", "countdown"],
convertSet: async (entity, key, value, meta) => {
const state = meta.message.state != null ? (utils.isString(meta.message.state) ? meta.message.state.toLowerCase() : undefined) : undefined;
const countdown = meta.message.countdown != null ? meta.message.countdown : undefined;
const result = {};
if (countdown !== undefined) {
// OnTime is a 16bit register and so might very well work up to 0xFFFF seconds but
// the Tuya documentation says that the maximum is 43200 (so 12 hours).
if (!Number.isInteger(countdown) || countdown < 0 || countdown > 12 * 3600) {
throw new Error("countdown must be an integer between 1 and 43200 (12 hours) or 0 to cancel");
}
}
// The order of the commands matters because 'on/off/toggle' cancels 'onWithTimedOff'.
if (state !== undefined) {
utils.validateValue(state, ["toggle", "off", "on"]);
await entity.command("genOnOff", state, {}, utils.getOptions(meta.mapped, entity));
if (state === "toggle") {
const currentState = meta.state[`state${meta.endpoint_name ? `_${meta.endpoint_name}` : ""}`];
if (currentState) {
result.state = currentState === "OFF" ? "ON" : "OFF";
}
}
else {
result.state = state.toUpperCase();
}
// A side effect of setting the state is to cancel any running coundown.
result.countdown = 0;
}
if (countdown !== undefined) {
// offwaittime is probably not used but according to the Tuya documentation, it should
// be set to the same value than ontime.
await entity.command("genOnOff", "onWithTimedOff", { ctrlbits: 0, ontime: countdown, offwaittime: countdown }, utils.getOptions(meta.mapped, entity));
if (result.state !== undefined) {
result.countdown = countdown;
}
}
return { state: result };
},
convertGet: async (entity, key, meta) => {
if (key === "state") {
await entity.read("genOnOff", ["onOff"]);
}
else if (key === "countdown") {
await entity.read("genOnOff", ["onTime"]);
}
},
},
inchingSwitch: {
key: ["inching_control_set"],
convertSet: async (entity, key, value, meta) => {
const endpoint = meta.device.getEndpoint(1);
await endpoint.command("manuSpecificTuya4", "setInchingSwitch",
// TODO: correct? seems it would take the `!(values instanceof Buffer)` codepath of ZH before
{ payload: Buffer.from(exports.valueConverter.inchingSwitch.to(value)) }, utils.getOptions(meta.mapped, endpoint));
return { state: { inching_control_set: value } };
},
},
cover_calibration: {
key: [
"calibration",
"calibration_to_open",
"calibration_to_close",
"calibration_time",
"calibration_time_to_open",
"calibration_time_to_close",
],
convertSet: async (entity, key, value, meta) => {
if (key.startsWith("calibration_time")) {
utils.assertNumber(value, key);
const calibration_time = value * 10;
if (key === "calibration_time" || key === "calibration_time_to_open") {
await entity.write("closuresWindowCovering", {
moesCalibrationTime: calibration_time,
});
}
else if (key === "calibration_time_to_close") {
await meta.device.getEndpoint(2).write("closuresWindowCovering", {
moesCalibrationTime: calibration_time,
});
}
return { state: { [key]: value } };
}
utils.assertString(value, key);
const lookup = { ON: 0, OFF: 1 };
value = value.toUpperCase();
const calibration = utils.getFromLookup(value, lookup);
if (key === "calibration" || key === "calibration_to_open") {
await entity.write("closuresWindowCovering", { tuyaCalibration: calibration });
}
else if (key === "calibration_to_close") {
await meta.device
.getEndpoint(2)
.write("closuresWindowCovering", { tuyaCalibration: calibration });
}
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
if (key === "calibration" || key === "calibration_to_open") {
await entity.read("closuresWindowCovering", ["tuyaCalibration"]);
}
else if (key === "calibration_to_close") {
await meta.device
.getEndpoint(2)
.read("closuresWindowCovering", ["tuyaCalibration"]);
}
else if (key === "calibration_time" || key === "calibration_time_to_open") {
await entity.read("closuresWindowCovering", ["moesCalibrationTime"]);
}
else if (key === "calibration_time_to_close") {
await meta.device
.getEndpoint(2)
.read("closuresWindowCovering", ["moesCalibrationTime"]);
}
},
},
operation_mode: {
key: ["operation_mode"],
convertSet: async (entity, key, value, meta) => {
// modes:
// 0 - 'command' mode. keys send commands. useful for group control
// 1 - 'event' mode. keys send events. useful for handling
utils.assertString(value, key);
const endpoint = meta.device.getEndpoint(1);
await endpoint.write("genOnOff", { tuyaOperationMode: utils.getFromLookup(value, { command: 0, event: 1 }) });
return { state: { operation_mode: value.toLowerCase() } };
},
convertGet: async (entity, key, meta) => {
const endpoint = meta.device.getEndpoint(1);
await endpoint.read("genOnOff", ["tuyaOperationMode"]);
},
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
TS110E_onoff_brightness: {
key: ["state", "brightness"],
convertSet: async (entity, key, value, meta) => {
const { message, state } = meta;
const brightnessKey = Object.keys(state).find((k) => k.startsWith("brightness_l")) && "ID" in entity ? `brightness_l${entity.ID}` : "brightness";
const stateKey = Object.keys(state).find((k) => k.startsWith("state_l")) && "ID" in entity ? `state_l${entity.ID}` : "state";
if (message.state === "OFF" || (message.state != null && message.brightness == null)) {
return await tz.on_off.convertSet(entity, key, value, meta);
}
if (message.brightness != null) {
// If state includes brightness assume we need to use a custom lookup
const brightness = utils.toNumber(message.brightness, "brightness");
// we allow at most 1 incase its a rounding/ float precision issue
const brightnessUnchanged = Math.abs(utils.mapNumberRange(brightness, 0, 254, 0, 254) - utils.toNumber(state[brightnessKey], "brightness")) <= 1;
// if the brightness is unchanged then we need to force it on due to weirdness with moveToLevelTuya
if (state[stateKey] === "OFF" && brightnessUnchanged) {
await entity.command("genOnOff", "on", {}, utils.getOptions(meta.mapped, entity));
}
else {
const level = utils.mapNumberRange(brightness, 0, 254, 0, 1000);
// set brightness
await entity.command("genLevelCtrl", "moveToLevelTuya", { level, transtime: 100 }, utils.getOptions(meta.mapped, entity));
}
return { state: { state: "ON", brightness } };
}
},
convertGet: async (entity, key, meta) => {
if (key === "state")
await tz.on_off.convertGet(entity, key, meta);
if (key === "brightness")
await entity.read("genLevelCtrl", [61440]);
},
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
TS110E_options: {
key: ["min_brightness", "max_brightness", "light_type", "switch_type"],
convertSet: async (entity, key, value, meta) => {
let payload = null;
if (key === "min_brightness" || key === "max_brightness") {
const id = key === "min_brightness" ? 64515 : 64516;
payload = { [id]: { value: utils.mapNumberRange(utils.toNumber(value, key), 1, 255, 0, 1000), type: 0x21 } };
}
else if (key === "light_type" || key === "switch_type") {
utils.assertString(value, "light_type/switch_type");
const lookup = key === "light_type" ? { led: 0, incandescent: 1, halogen: 2 } : { momentary: 0, toggle: 1, state: 2 };
payload = { 64514: { value: lookup[value], type: 0x20 } };
}
await entity.write("genLevelCtrl", payload, utils.getOptions(meta.mapped, entity));
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
let id = null;
if (key === "min_brightness")
id = 64515;
if (key === "max_brightness")
id = 64516;
if (key === "light_type" || key === "switch_type")
id = 64514;
await entity.read("genLevelCtrl", [id]);
},
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
TS110E_light_onoff_brightness: {
...tz.light_onoff_brightness,
convertSet: async (entity, key, value, meta) => {
const { message } = meta;
if (message.state === "ON" || (typeof message.brightness === "number" && message.brightness > 1)) {
// Does not turn off with physical press when turned on with just moveToLevelWithOnOff, required on before.
// https://github.com/Koenkk/zigbee2mqtt/issues/15902#issuecomment-1382848150
await entity.command("genOnOff", "on", {}, utils.getOptions(meta.mapped, entity));
}
return await tz.light_onoff_brightness.convertSet(entity, key, value, meta);
},
},
cover_reversal: {
key: ["motor_reversal"],
convertSet: async (entity, key, value, meta) => {
utils.assertString(value, key);
const lookup = { ON: 1, OFF: 0 };
value = value.toUpperCase();
const reversal = utils.getFromLookup(value, lookup);
await entity.write("closuresWindowCovering", { tuyaMotorReversal: reversal });
return { state: { motor_reversal: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("closuresWindowCovering", ["tuyaMotorReversal"]);
},
},
moes_cover_calibration: {
key: ["calibration_time"],
convertSet: async (entity, key, value, meta) => {
utils.assertNumber(value);
const calibration = value * 10;
await entity.write("closuresWindowCovering", { moesCalibrationTime: calibration });
return { state: { calibration_time: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("closuresWindowCovering", ["moesCalibrationTime"]);
},
},
led_control: {
key: ["brightness", "color", "color_temp"],
options: [exposes.options.color_sync()],
convertSet: async (entity, key, value, meta) => {
if (key === "brightness" &&
meta.state.color_mode === constants.colorModeLookup[2] &&
meta.message.color == null &&
meta.message.color_temp == null) {
const level = Number(value);
await entity.command("genLevelCtrl", "moveToLevel", { level, transtime: 0, optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity));
globalStore.putValue(entity, "brightness", level);
return { state: { brightness: level } };
}
if (key === "brightness" && utils.isNumber(meta.message.color_temp)) {
const level = Number(value);
await entity.command("lightingColorCtrl", "tuyaRgbMode", { enable: 0 });
await entity.command("lightingColorCtrl", "moveToColorTemp", {
colortemp: utils.mapNumberRange(meta.message.color_temp, 500, 154, 0, 254),
transtime: 0,
optionsMask: 0,
optionsOverride: 0,
}, utils.getOptions(meta.mapped, entity));
await entity.command("genLevelCtrl", "moveToLevel", { level, transtime: 0, optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity));
globalStore.putValue(entity, "brightness", level);
const newState = {
brightness: level,
color_mode: constants.colorModeLookup[2],
color_temp: meta.message.color_temp,
};
return { state: libColor.syncColorState(newState, meta.state, entity, meta.options) };
}
if (key === "color_temp") {
utils.assertNumber(value, key);
const level = globalStore.getValue(entity, "brightness") || 100;
await entity.command("lightingColorCtrl", "tuyaRgbMode", { enable: 0 });
await entity.command("lightingColorCtrl", "moveToColorTemp", { colortemp: utils.mapNumberRange(value, 500, 154, 0, 254), transtime: 0, optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity));
await entity.command("genLevelCtrl", "moveToLevel", { level, transtime: 0, optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity));
const newState = {
brightness: level,
color_mode: constants.colorModeLookup[2],
color_temp: value,
};
return { state: libColor.syncColorState(newState, meta.state, entity, meta.options) };
}
const zclData = {
brightness: globalStore.getValue(entity, "brightness") || 100,
// @ts-expect-error ignore
hue: utils.mapNumberRange(meta.state.color.h, 0, 360, 0, 254) || 100,
// @ts-expect-error ignore
saturation: utils.mapNumberRange(meta.state.color.s, 0, 100, 0, 254) || 100,
transtime: 0,
};
if (utils.isObject(value)) {
if (value.h) {
zclData.hue = utils.mapNumberRange(value.h, 0, 360, 0, 254);
}
if (value.hue) {
zclData.hue = utils.mapNumberRange(value.hue, 0, 360, 0, 254);
}
if (value.s) {
zclData.saturation = utils.mapNumberRange(value.s, 0, 100, 0, 254);
}
if (value.saturation) {
zclData.saturation = utils.mapNumberRange(value.saturation, 0, 100, 0, 254);
}
if (value.b) {
zclData.brightness = Number(value.b);
}
if (value.brightness) {
zclData.brightness = Number(value.brightness);
}
if (typeof value === "number") {
zclData.brightness = value;
}
}
if (meta.message.color != null) {
if (utils.isObject(meta.message.color)) {
if (meta.message.color.h) {
zclData.hue = utils.mapNumberRange(meta.message.color.h, 0, 360, 0, 254);
}
if (meta.message.color.s) {
zclData.saturation = utils.mapNumberRange(meta.message.color.s, 0, 100, 0, 254);
}
if (meta.message.color.b) {
zclData.brightness = Number(meta.message.color.b);
}
if (meta.message.color.brightness) {
zclData.brightness = Number(meta.message.color.brightness);
}
}
}
await entity.command("lightingColorCtrl", "tuyaRgbMode", { enable: 1 });
await entity.command("lightingColorCtrl", "tuyaMoveToHueAndSaturationBrightness", zclData, utils.getOptions(meta.mapped, entity));
globalStore.putValue(entity, "brightness", zclData.brightness);
const newState = {
brightness: zclData.brightness,
color: {
h: utils.mapNumberRange(zclData.hue, 0, 254, 0, 360),
hue: utils.mapNumberRange(zclData.hue, 0, 254, 0, 360),
s: utils.mapNumberRange(zclData.saturation, 0, 254, 0, 100),
saturation: utils.mapNumberRange(zclData.saturation, 0, 254, 0, 100),
},
color_mode: constants.colorModeLookup[0],
};
return { state: libColor.syncColorState(newState, meta.state, entity, meta.options) };
},
convertGet: async (entity, key, meta) => {
await entity.read("lightingColorCtrl", [
"currentHue",
"currentSaturation",
"tuyaBrightness",
"tuyaRgbMode",
"colorTemperature",
]);
},
},
relay_din_led_indicator: {
key: ["indicator_mode"],
convertSet: async (entity, key, value, meta) => {
utils.assertString(value, key);
value = value.toLowerCase();
const lookup = { off: 0x00, on_off: 0x01, off_on: 0x02 };
const payload = utils.getFromLookup(value, lookup);
await entity.write("genOnOff", { 32769: { value: payload, type: 0x30 } });
return { state: { indicator_mode: value } };
},
},
led_controller: {
key: ["state", "color"],
convertSet: async (entity, key, value, meta) => {
if (key === "state") {
utils.assertString(value, key);
if (value.toLowerCase() === "off") {
await entity.command("genOnOff", "offWithEffect", { effectid: 0x01, effectvariant: 0x01 }, utils.getOptions(meta.mapped, entity));
}
else {
await entity.command("genLevelCtrl", "moveToLevelWithOnOff", { level: 255, transtime: 0, optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity));
}
return { state: { state: value.toUpperCase() } };
}
if (key === "color") {
utils.assertObject(value);
const hue = utils.mapNumberRange(value.h, 0, 360, 0, 254);
const saturation = utils.mapNumberRange(value.s, 0, 100, 0, 254);
const transtime = 0;
const direction = 0;
await entity.command("lightingColorCtrl", "moveToHue", { hue, transtime, direction, optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity));
await entity.command("lightingColorCtrl", "moveToSaturation", { saturation, transtime, optionsMask: 0, optionsOverride: 0 }, utils.getOptions(meta.mapped, entity));
}
},
convertGet: async (entity, key, meta) => {
if (key === "state") {
await entity.read("genOnOff", ["onOff"]);
}
else if (key === "color") {
await entity.read("lightingColorCtrl", ["currentHue", "currentSaturation"]);
}
},
},
ts0216_duration: {
key: ["duration"],
convertSet: async (entity, key, value, meta) => {
await entity.write("ssIasWd", { maxDuration: value });
},
convertGet: async (entity, key, meta) => {
await entity.read("ssIasWd", ["maxDuration"]);
},
},
ts0216_volume: {
key: ["volume"],
convertSet: async (entity, key, value, meta) => {
utils.assertNumber(value);
if (["_TYZB01_sbpc1zrb"].includes(meta.device.manufacturerName)) {
const volume = value === 0 ? 0 : utils.mapNumberRange(value, 1, 100, 100, 33);
await entity.write("ssIasWd", { 2: { value: volume, type: 0x20 } });
return;
}
await entity.write("ssIasWd", { 2: { value: utils.mapNumberRange(value, 0, 100, 100, 10), type: 0x20 } });
},
convertGet: async (entity, key, meta) => {
await entity.read("ssIasWd", [0x0002]);
},
},
ts0216_alarm: {
key: ["alarm"],
convertSet: async (entity, key, value, meta) => {
const info = value ? (2 << 4) + (1 << 2) + 0 : 0;
await entity.command("ssIasWd", "startWarning", { startwarninginfo: info, warningduration: 0, strobedutycycle: 0, strobelevel: 3 }, utils.getOptions(meta.mapped, entity));
},
},
};
exports.tz = tuyaTz;
const tuyaFz = {
brightness: {
cluster: "genLevelCtrl",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data["61440"] !== undefined) {
const property = utils.postfixWithEndpointName("brightness", msg, model, meta);
return { [property]: utils.mapNumberRange(msg.data["61440"], 0, 1000, 0, 255) };
}
},
},
power_on_behavior_1: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.moesStartUpOnOff !== undefined) {
const lookup = { 0: "off", 1: "on", 2: "previous" };
const property = utils.postfixWithEndpointName("power_on_behavior", msg, model, meta);
return { [property]: lookup[msg.data.moesStartUpOnOff] };
}
},
},
power_on_behavior_2: {
cluster: "manuSpecificTuya3",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const attribute = "powerOnBehavior";
const lookup = { 0: "off", 1: "on", 2: "previous" };
if (msg.data[attribute] !== undefined) {
const property = utils.postfixWithEndpointName("power_on_behavior", msg, model, meta);
return { [property]: lookup[msg.data[attribute]] };
}
},
},
power_outage_memory: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.moesStartUpOnOff !== undefined) {
const lookup = { 0: "off", 1: "on", 2: "restore" };
const property = utils.postfixWithEndpointName("power_outage_memory", msg, model, meta);
return { [property]: lookup[msg.data.moesStartUpOnOff] };
}
},
},
switch_type: {
cluster: "manuSpecificTuya3",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.switchType !== undefined) {
const lookup = { 0: "toggle", 1: "state", 2: "momentary" };
utils.assertNumber(msg.data.switchType);
return { switch_type: lookup[msg.data.switchType] };
}
},
},
switch_type_curtain: {
cluster: "manuSpecificTuya3",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.switchType !== undefined) {
const lookup = { 0: "flip-switch", 1: "sync-switch", 2: "button-switch", 3: "button2-switch" };
utils.assertNumber(msg.data.switchType);
return { switch_type_curtain: lookup[msg.data.switchType] };
}
},
},
switch_type_button: {
cluster: "manuSpecificTuya3",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.switchType !== undefined) {
const lookup = { 0: "release", 1: "press" };
utils.assertNumber(msg.data.switchType);
return { switch_type_button: lookup[msg.data.switchType] };
}
},
},
backlight_mode_low_medium_high: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.tuyaBacklightMode !== undefined) {
const value = msg.data.tuyaBacklightMode;
const backlightLookup = { 0: "low", 1: "medium", 2: "high" };
return { backlight_mode: backlightLookup[value] };
}
},
},
backlight_mode_off_normal_inverted: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.tuyaBacklightMode !== undefined) {
return { backlight_mode: utils.getFromLookup(msg.data.tuyaBacklightMode, { 0: "off", 1: "normal", 2: "inverted" }) };
}
},
},
backlight_mode_off_on: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.tuyaBacklightSwitch !== undefined) {
return { backlight_mode: utils.getFromLookup(msg.data.tuyaBacklightSwitch, { 0: "OFF", 1: "ON" }) };
}
},
},
indicator_mode: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.tuyaBacklightMode !== undefined) {
return { indicator_mode: utils.getFromLookup(msg.data.tuyaBacklightMode, { 0: "off", 1: "off/on", 2: "on/off", 3: "on" }) };
}
},
},
indicator_mode_none_relay_pos: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.tuyaBacklightMode !== undefined) {
return { indicator_mode: utils.getFromLookup(msg.data.tuyaBacklightMode, { 0: "none", 1: "relay", 2: "pos" }) };
}
},
},
child_lock: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data["32768"] !== undefined) {
const value = msg.data["32768"];
return { child_lock: value ? "LOCK" : "UNLOCK" };
}
},
},
min_brightness_attribute: {
cluster: "genLevelCtrl",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data[0xfc00] !== undefined) {
const property = utils.postfixWithEndpointName("min_brightness", msg, model, meta);
const value = Number.parseInt(msg.data[0xfc00].toString(16).slice(0, 2), 16);
return { [property]: value };
}
},
},
datapoints: {
cluster: "manuSpecificTuya",
type: ["commandDataResponse", "commandDataReport", "commandActiveStatusReport", "commandActiveStatusReportAlt"],
convert: (model, msg, publish, options, meta) => {
if (utils.hasAlreadyProcessedMessage(msg, model))
return;
const result = {};
if (!model.meta?.tuyaDatapoints)
throw new Error("No datapoints map defined");
const datapoints = model.meta.tuyaDatapoints;
for (const dpValue of msg.data.dpValues) {
const dpId = dpValue.dp;
const dpEntry = datapoints.find((d) => d[0] === dpId);
const value = getDataValue(dpValue);
if (dpEntry?.[2]?.from) {
if (dpEntry[1]) {
result[dpEntry[1]] = dpEntry[2].from(value, meta, options, publish, msg);
}
else {
Object.assign(result, dpEntry[2].from(value, meta, options, publish, msg));
}
}
else {
logger_1.logger.debug(`Datapoint ${dpId} not defined for '${meta.device.manufacturerName}' with value ${value}`, NS);
}
}
return result;
},
},
on_off_action: {
cluster: "genOnOff",
type: "commandTuyaAction",
convert: (model, msg, publish, options, meta) => {
if (utils.hasAlreadyProcessedMessage(msg, model, msg.data[0]))
return;
const clickMapping = { 0: "single", 1: "double", 2: "hold" };
const buttonMapping = { 1: "1", 2: "2", 3: "3", 4: "4", 5: "5", 6: "6", 7: "7", 8: "8" };
// TS004F has single endpoint, TS0041A/TS0041 can have multiple but have just one button
const button = msg.device.endpoints.length === 1 || ["TS0041A", "TS0041"].includes(msg.device.modelID) ? "" : `${buttonMapping[msg.endpoint.ID]}_`;
return { action: `${button}${clickMapping[msg.data.value]}` };
},
},
on_off_countdown: {
// While a countdown is in progress, the device will report onTime at all multiples of 60.
// More reportings can be configured for 'onTime` but they will happen independently of
// the builtin 60s reporting.
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.onTime !== undefined) {
const payload = {};
const property = utils.postfixWithEndpointName("countdown", msg, model, meta);
const countdown = msg.data.onTime;
payload[property] = countdown;
return payload;
}
},
},
inchingSwitch: {
cluster: "manuSpecificTuya4",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.inching !== undefined) {
const payload = {};
const value = exports.valueConverter.inchingSwitch.from(msg.data.inching);
payload.inching_control_set = value;
return payload;
}
},
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
TS011F_electrical_measurement: {
...fz.electrical_measurement,
convert: (model, msg, publish, options, meta) => {
const result = fz.electrical_measurement.convert(model, msg, publish, options, meta) ?? {};
const lookup = {
power: "activePower",
current: "rmsCurrent",
voltage: "rmsVoltage",
};
// Wait 5 seconds before reporting a 0 value as this could be an invalid measurement.
// https://github.com/Koenkk/zigbee2mqtt/issues/16709#issuecomment-1509599046
if (result) {
for (const key of ["power", "current", "voltage"]) {
if (key in result) {
const value = result[key];
clearTimeout(globalStore.getValue(msg.endpoint, key));
if (value === 0) {
const configuredReporting = msg.endpoint.configuredReportings.find((c) => c.cluster.name === "haElectricalMeasurement" && c.attribute.name === lookup[key]);
const time = (configuredReporting ? configuredReporting.minimumReportInterval : 5) * 2 + 1;
const timer = setTimeout(() => {
const payload = { [key]: value };
// Device takes a lot of time to report power 0 in some cases. When current == 0 we can assume power == 0
// https://github.com/Koenkk/zigbee2mqtt/discussions/19680#discussioncomment-7868445
if (key === "current") {
payload.power = 0;
}
publish(payload);
}, time * 1000).unref();
globalStore.putValue(msg.endpoint, key, timer);
delete result[key];
}
}
}
}
// Device takes a lot of time to report power 0 in some cases. When the state is OFF we can assume power == 0
// https://github.com/Koenkk/zigbee2mqtt/discussions/19680#discussioncomment-7868445
if (meta.state.state === "OFF") {
result.power = 0;
}
return result;
},
},
cover_options: {
cluster: "closuresWindowCovering",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data.tuyaMovingState !== undefined) {
const value = msg.data.tuyaMovingState;
const movingLookup = { 0: "UP", 1: "STOP", 2: "DOWN" };
result[utils.postfixWithEndpointName("moving", msg, model, meta)] = movingLookup[value];
}
if (msg.data.tuyaCalibration !== undefined) {
const value = msg.data.tuyaCalibration;
const calibrationLookup = { 0: "ON", 1: "OFF" };
result[utils.postfixWithEndpointName("calibration", msg, model, meta)] = calibrationLookup[value];
}
if (msg.data.tuyaMotorReversal !== undefined) {
const value = msg.data.tuyaMotorReversal;
const reversalLookup = { 0: "OFF", 1: "ON" };
result[utils.postfixWithEndpointName("motor_reversal", msg, model, meta)] = reversalLookup[value];
}
if (msg.data.moesCalibrationTime !== undefined) {
const value = msg.data.moesCalibrationTime / 10.0;
if (["_TZ3000_cet6ch1r", "_TZ3000_5iixzdo7"].includes(meta.device.manufacturerName)) {
const endpoint = msg.endpoint.ID;
const calibrationLookup = { 1: "to_open", 2: "to_close" };
result[utils.postfixWithEndpointName(`calibration_time_${calibrationLookup[endpoint]}`, msg, model, meta)] = value;
}
else {
result[utils.postfixWithEndpointName("calibration_time", msg, model, meta)] = value;
}
}
return result;
},
},
cover_options_2: {
cluster: "closuresWindowCovering",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data.moesCalibrationTime !== undefined) {
const value = msg.data.moesCalibrationTime / 100;
result[utils.postfixWithEndpointName("calibration_time", msg, model, meta)] = value;
}
if (msg.data.tuyaMotorReversal !== undefined) {
const value = msg.data.tuyaMotorReversal;
const reversalLookup = { 0: "OFF", 1: "ON" };
result[utils.postfixWithEndpointName("motor_reversal", msg, model, meta)] = reversalLookup[value];
}
return result;
},
},
operation_mode: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.tuyaOperationMode !== undefined) {
const value = msg.data.tuyaOperationMode;
const lookup = { 0: "command", 1: "event" };
return { operation_mode: lookup[value] };
}
},
},
switch_scene: {
cluster: "genOnOff",
type: "commandTuyaAction",
convert: (model, msg, publish, options, meta) => {
if (utils.hasAlreadyProcessedMessage(msg, model))
return;
// Since it is a non standard ZCL command, no default response is send from zigbee-herdsman
// Send the defaultResponse here, otherwise the second button click delays.
// https://github.com/Koenkk/zigbee2mqtt/issues/8149
return { action: "switch_scene", action_scene: msg.data.value };
},
},
multi_action: {
cluster: "genOnOff",
type: ["commandTuyaAction", "commandTuyaAction2"],
convert: (model, msg, publish, options, meta) => {
if (utils.hasAlreadyProcessedMessage(msg, model))
return;
// biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
let action;
if (msg.type === "commandTuyaAction") {
const lookup = { 0: "single", 1: "double", 2: "hold" };
action = lookup[msg.data.value];
}
else if (msg.type === "commandTuyaAction2") {
const lookup = { 0: "rotate_right", 1: "rotate_left" };
action = lookup[msg.data.value];
}
return { action };
},
},
led_controller: {
cluster: "lightingColorCtrl",
type: ["attributeReport", "readResponse"],
options: [exposes.options.color_sync()],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data.colorTemperature !== undefined) {
const value = Number(msg.data.colorTemperature);
const color_temp = utils.postfixWithEndpointName("color_temp", msg, model, meta);
result[color_temp] = value;
}
if (msg.data.tuyaBrightness !== undefined) {
const brightness = utils.postfixWithEndpointName("brightness", msg, model, meta);
result[brightness] = msg.data.tuyaBrightness;
}
if (msg.data.tuyaRgbMode !== undefined) {
const color_mode = utils.postfixWithEndpointName("color_mode", msg, model, meta);
if (msg.data.tuyaRgbMode === 1) {
result[color_mode] = constants.colorModeLookup[0];
}
else {
result[color_mode] = constants.colorModeLookup[2];
}
}
const color = utils.postfixWithEndpointName("color", msg, model, meta);
result[color] = {};
if (msg.data.currentHue !== undefined) {
result[color].hue = utils.mapNumberRange(msg.data.currentHue, 0, 254, 0, 360);
result[color].h = result[color].hue;
}
if (msg.data.currentSaturation !== undefined) {
result[color].saturation = utils.mapNumberRange(msg.data.currentSaturation, 0, 254, 0, 100);
result[color].s = result[color].saturation;
}
// Use postfixWithEndpointName with an empty value to get just the postfix that
// can be added to the result keys.
const epPostfix = utils.postfixWithEndpointName("", msg, model, meta);
return Object.assign(result, libColor.syncColorState(result, meta.state, msg.endpoint, options, epPostfix));
},
},
relay_din_led_indicator: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const property = 0x8001;
if (msg.data[property] !== undefined) {
const dict = { 0: "off", 1: "on_off", 2: "off_on" };
const value = msg.data[property];
if (dict[value] !== undefined) {
return { [utils.postfixWithEndpointName("indicator_mode", msg, model, meta)]: dict[value] };
}
}
},
},
TS110E: {
cluster: "genLevelCtrl",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data["64515"] !== undefined) {
result.min_brightness = utils.mapNumberRange(msg.data["64515"], 0, 1000, 1, 255);
}
if (msg.data["64516"] !== undefined) {
result.max_brightness = utils.mapNumberRange(msg.data["64516"], 0, 1000, 1, 255);
}
if (msg.data["61440"] !== undefined) {
const propertyName = utils.postfixWithEndpointName("brightness", msg, model, meta);
result[propertyName] = utils.mapNumberRange(msg.data["61440"], 0, 1000, 0, 255);
}
return result;
},
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
TS110E_light_type: {
cluster: "genLevelCtrl",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data["64514"] !== undefined) {
const lookup = { 0: "led", 1: "incandescent", 2: "halogen" };
result.light_type = lookup[msg.data["64514"]];
}
return result;
},
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
TS110E_switch_type: {
cluster: "genLevelCtrl",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data["64514"] !== undefined) {
const lookup = { 0: "momentary", 1: "toggle", 2: "state" };
const propertyName = utils.postfixWithEndpointName("switch_type", msg, model, meta);
result[propertyName] = lookup[msg.data["64514"]];
}
return result;
},
},
ts0216_siren: {
cluster: "ssIasWd",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data.maxDuration !== undefined)
result.duration = msg.data.maxDuration;
if (msg.data["2"] !== undefined) {
result.volume = utils.mapNumberRange(msg.data["2"], 100, 10, 0, 100);
}
if (["_TYZB01_sbpc1zrb"].includes(meta.device.manufacturerName) && typeof msg.data["2"] === "number") {
const volData = msg.data["2"];
result.volume = volData === 0 ? 0 : utils.mapNumberRange(volData, 100, 33, 1, 100);
}
if (msg.data["61440"] !== undefined) {
result.alarm = msg.data["61440"] !== 0;
}
return result;
},
},
};
exports.fz = tuyaFz;
function getHandlersForDP(name, dp, type, converter, readOnly, skip, endpoint, useGlobalSequence) {
const keyName = endpoint ? `${name}_${endpoint}` : name;
const fromZigbee = [
{
cluster: "manuSpecificTuya",
type: ["commandDataResponse", "commandDataReport", "commandActiveStatusReport", "commandActiveStatusReportAlt"],
convert: (model, msg, publish, options, meta) => {
const dpValue = msg.data.dpValues.find((d) => d.dp === dp);
if (dpValue) {
return { [keyName]: converter.from(getDataValue(dpValue)) };
}
},
},
];
const toZigbee = readOnly
? undefined
: [
{
key: [name],
endpoints: endpoint ? [endpoint] : undefined,
convertSet: async (entity, key, value, meta) => {
// A set converter is only called once; therefore we need to loop
const state = {};
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
for (const [attr, value] of Object.entries(meta.message)) {
const convertedKey = meta.mapped.meta?.multiEndpoint && meta.endpoint_name && !attr.startsWith(`${key}_`)
? `${attr}_${meta.endpoint_name}`
: attr;
// logger.debug(`key: ${key}, convertedKey: ${convertedKey}, keyName: ${keyName}`);
if (convertedKey !== keyName)
continue;
if (skip?.(meta))
continue;
const convertedValue = await converter.to(value, meta);
const sendCommand = utils.getMetaValue(entity, meta.mapped, "tuyaSendCommand", undefined, "dataRequest");
const seq = useGlobalSequence ? undefined : 1;
// logger.debug(`dp: ${dp}, value: ${value}, convertedValue: ${convertedValue}`);
if (convertedValue === undefined) {
// conversion done inside converter, ignore.
}
else if (type === exports.dataTypes.bool) {
await sendDataPointBool(entity, dp, convertedValue, sendCommand, seq);
}
else if (type === exports.dataTypes.number) {
await sendDataPointValue(entity, dp, convertedValue, sendCommand, seq);
}
else if (type === exports.dataTypes.string) {
await sendDataPointStringBuffer(entity, dp, convertedValue, sendCommand, seq);
}
else if (type === exports.dataTypes.raw) {
await sendDataPointRaw(entity, dp, Buffer.from(convertedValue), sendCommand, seq);
}
else if (type === exports.dataTypes.enum) {
await sendDataPointEnum(entity, dp, convertedValue, sendCommand, seq);
}
else if (type === exports.dataTypes.bitmap) {
await sendDataPointBitmap(entity, dp, convertedValue, sendCommand, seq);
}
else {
throw new Error(`Don't know how to send type '${typeof convertedValue}'`);
}
state[key] = value;
}
return { state };
},
},
];
return [fromZigbee, toZigbee];
}
const tuyaModernExtend = {
electricityMeasurementPoll(args = {}) {
const { electricalMeasurement = true, metering = false, optionDescription = undefined } = args;
let option = exposes.options.measurement_poll_interval();
if (optionDescription !== undefined) {
option = option.withDescription(optionDescription);
}
return modernExtend.poll({
key: "measurement",
option,
defaultIntervalSeconds: 60,
poll: async (device) => {
const endpoint = device.getEndpoint(1);
if (typeof electricalMeasurement === "boolean" ? electricalMeasurement : electricalMeasurement(device)) {
await endpoint.read("haElectricalMeasurement", ["rmsVoltage", "rmsCurrent", "activePower"]);
}
if (typeof metering === "boolean" ? metering : metering(device)) {
await endpoint.read("seMetering", ["currentSummDelivered"]);
}
},
});
},
dpTHZBSettings() {
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 = getHandlersForDP("auto_settings", 0x77, exports.dataTypes.string, {
from: (value) => {
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, 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 = {}) {
const { dp = false, queryOnDeviceAnnounce = false, queryOnConfigure = false, bindBasicOnConfigure = false, queryIntervalSeconds = undefined, mcuVersionRequestOnConfigure = false,
// Allow force updating for device with a very bad clock
// Every hour when a message is received the time will be updated.
forceTimeUpdates = false, timeStart = "off",
// Disable by default as with many Tuya devices it doesn't work well.
// https://github.com/Koenkk/zigbee2mqtt/issues/28367#issuecomment-3363460429
respondToMcuVersionResponse = false, } = args;
const fzConverter = {
type: [
"commandMcuSyncTime",
"commandMcuVersionResponse",
"commandMcuGatewayConnectionStatus",
"commandDataResponse",
"commandDataReport",
"commandActiveStatusReport",
"commandActiveStatusReportAlt",
],
cluster: "manuSpecificTuya",
convert: (model, msg, publish, options, meta) => {
let forceTimeUpdate = false;
if (forceTimeUpdates) {
const nextLocalTimeUpdate = globalStore.getValue(msg.device, "nextLocalTimeUpdate");
forceTimeUpdate = nextLocalTimeUpdate == null || nextLocalTimeUpdate < Date.now();
}
if (timeStart !== "off" && (msg.type === "commandMcuSyncTime" || forceTimeUpdate)) {
globalStore.putValue(msg.device, "nextLocalTimeUpdate", Date.now() + 3600 * 1000);
const offset = timeStart === "2000" ? constants.OneJanuary2000 : 0;
const utcTime = Math.round((Date.now() - offset) / 1000);
const localTime = utcTime - new Date().getTimezoneOffset() * 60;
const payload = {
payloadSize: 8,
payload: [...convertDecimalValueTo4ByteHexArray(utcTime), ...convertDecimalValueTo4ByteHexArray(localTime)],
};
msg.endpoint
.command("manuSpecificTuya", "mcuSyncTime", payload, {})
.catch((error) => logger_1.logger.error(`Failed to sync time with '${msg.device.ieeeAddr}' (${error})`, NS));
}
else if (respondToMcuVersionResponse && msg.type === "commandMcuVersionResponse") {
msg.endpoint
.command("manuSpecificTuya", "mcuVersionRequest", { seq: 0x0002 })
.catch((error) => logger_1.logger.error(`Failed respond to version response '${msg.device.ieeeAddr}' (${error})`, NS));
}
else if (msg.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.
msg.endpoint
.command("manuSpecificTuya", "mcuGatewayConnectionStatus", { payloadSize: 1, payload: 1 }, {})
.catch((error) => logger_1.logger.error(`Failed respond to gateway connection status '${msg.device.ieeeAddr}' (${error})`, NS));
}
},
};
const result = {
configure: [exports.configureMagicPacket],
isModernExtend: true,
fromZigbee: [fzConverter],
toZigbee: [],
};
if (queryOnConfigure) {
result.configure.push(exports.configureQuery);
}
if (mcuVersionRequestOnConfigure) {
result.configure.push(exports.configureMcuVersionRequest);
}
if (bindBasicOnConfigure) {
result.configure.push(exports.configureBindBasic);
}
if (queryOnDeviceAnnounce || queryIntervalSeconds !== undefined) {
result.onEvent = [
(event) => {
// Some devices require a dataQuery on deviceAnnounce, otherwise they don't report any data
if (queryOnDeviceAnnounce && event.type === "deviceAnnounce") {
event.data.device.endpoints[0]
.command("manuSpecificTuya", "dataQuery", {})
.catch((error) => logger_1.logger.error(`Failed to query '${event.data.device.ieeeAddr}' on device announce (${error})`, NS));
}
if (queryIntervalSeconds !== undefined) {
if (event.type === "stop") {
clearTimeout(globalStore.getValue(event.data.ieeeAddr, "query_interval"));
globalStore.clearValue(event.data.ieeeAddr, "query_interval");
}
else if (event.type === "start") {
const setTimer = () => {
const timer = setTimeout(() => {
event.data.device.endpoints[0]
.command("manuSpecificTuya", "dataQuery", {})
.catch((error) => logger_1.logger.error(`Failed to query '${event.data.device.ieeeAddr}' on interval (${error})`, NS));
if (globalStore.getValue(event.data.device.ieeeAddr, "query_interval") === timer) {
setTimer();
}
}, queryIntervalSeconds * 1000).unref();
globalStore.putValue(event.data.device.ieeeAddr, "query_interval", timer);
};
setTimer();
}
}
},
];
}
const tuyaGenBasic = tuyaClusters.addTuyaGenBasicCluster();
const tuyaGenGroups = tuyaClusters.addTuyaGenGroupsCluster();
const tuyaGenOnOff = tuyaClusters.addTuyaGenOnOffCluster();
const tuyaGenLevelCtrl = tuyaClusters.addTuyaGenLevelCtrlCluster();
const customCluster2 = tuyaClusters.addManuSpecificTuya2Cluster();
const customCluster3 = tuyaClusters.addManuSpecificTuya3Cluster();
result.onEvent = [
...(tuyaGenBasic.onEvent ?? []),
...(tuyaGenGroups.onEvent ?? []),
...(tuyaGenOnOff.onEvent ?? []),
...(tuyaGenLevelCtrl.onEvent ?? []),
...(customCluster2.onEvent ?? []),
...(customCluster3.onEvent ?? []),
...(result.onEvent ?? []),
];
result.configure = [
...(tuyaGenBasic.configure ?? []),
...(tuyaGenGroups.configure ?? []),
...(tuyaGenOnOff.configure ?? []),
...(tuyaGenLevelCtrl.configure ?? []),
...(customCluster2.configure ?? []),
...(customCluster3.configure ?? []),
...(result.configure ?? []),
];
if (dp) {
result.fromZigbee.push(tuyaFz.datapoints);
result.toZigbee.push(tuyaTz.datapoints);
}
return result;
},
dpEnumLookup(args) {
const { name, dp, type, lookup, description, readOnly, endpoint, expose, skip } = args;
let exp;
if (expose) {
exp = expose;
}
else {
exp = new exposes.Enum(name, readOnly ? ea.STATE : ea.STATE_SET, Object.keys(lookup)).withDescription(description);
}
if (endpoint)
exp = exp.withEndpoint(endpoint);
const handlers = getHandlersForDP(name, dp, type, {
from: (value) => utils.getFromLookupByValue(value, lookup),
to: (value) => utils.getFromLookup(value, lookup),
}, readOnly, skip, endpoint);
return { exposes: [exp], fromZigbee: handlers[0], toZigbee: handlers[1], isModernExtend: true };
},
dpBinary(args) {
const { name, dp, type, valueOn, valueOff, description, readOnly, endpoint, expose, skip } = args;
let exp;
if (expose) {
exp = expose;
}
else {
exp = e.binary(name, readOnly ? ea.STATE : ea.STATE_SET, valueOn[0], valueOff[0]).withDescription(description);
}
if (endpoint)
exp = exp.withEndpoint(endpoint);
const handlers = getHandlersForDP(name, dp, type, {
from: (value) => (value === valueOn[1] ? valueOn[0] : valueOff[0]),
to: (value) => (value === valueOn[0] ? valueOn[1] : valueOff[1]),
}, readOnly, skip, endpoint);
return { exposes: [exp], fromZigbee: handlers[0], toZigbee: handlers[1], isModernExtend: true };
},
dpNumeric(args) {
const { name, dp, type, description, readOnly, endpoint, unit, valueMax, valueMin, valueStep, scale, expose, skip } = args;
let exp;
if (expose) {
exp = expose;
}
else {
exp = e.numeric(name, readOnly ? ea.STATE : ea.STATE_SET).withDescription(description);
}
if (endpoint)
exp = exp.withEndpoint(endpoint);
if (unit)
exp = exp.withUnit(unit);
if (valueMin !== undefined)
exp = exp.withValueMin(valueMin);
if (valueMax !== undefined)
exp = exp.withValueMax(valueMax);
if (valueStep !== undefined)
exp = exp.withValueStep(valueStep);
// biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
let converter;
if (scale === undefined) {
converter = exports.valueConverterBasic.raw();
}
else {
if (Array.isArray(scale)) {
converter = exports.valueConverterBasic.scale(scale[0], scale[1], scale[2], scale[3]);
}
else {
converter = exports.valueConverterBasic.divideBy(scale);
}
}
const handlers = getHandlersForDP(name, dp, type, converter, readOnly, skip, endpoint);
return { exposes: [exp], fromZigbee: handlers[0], toZigbee: handlers[1], isModernExtend: true };
},
dpLight(args) {
const { state, brightness, min, max, colorTemp, endpoint } = args;
let exp = e.light_brightness().setAccess("state", ea.STATE_SET).setAccess("brightness", ea.STATE_SET);
// biome-ignore lint/suspicious/noExplicitAny: too messy...
let fromZigbee = [];
let toZigbee = [];
let ext;
if (min) {
exp = exp.withMinBrightness().setAccess("min_brightness", ea.STATE_SET);
}
if (max) {
exp = exp.withMaxBrightness().setAccess("max_brightness", ea.STATE_SET);
}
if (colorTemp) {
exp = exp.withColorTemp(colorTemp.range).setAccess("color_temp", ea.STATE_SET);
}
// if (color) {
// exp = exp.withColor(['hs']).setAccess('color_hs', ea.STATE_SET);
// }
if (endpoint)
exp = exp.withEndpoint(endpoint);
ext = tuyaModernExtend.dpBinary({
name: "state",
dp: state.dp,
type: state.type,
valueOn: state.valueOn,
valueOff: state.valueOff,
skip: state.skip,
endpoint: endpoint,
});
fromZigbee = [...fromZigbee, ...ext.fromZigbee];
toZigbee = [...toZigbee, ...ext.toZigbee];
ext = tuyaModernExtend.dpNumeric({ name: "brightness", dp: brightness.dp, type: brightness.type, scale: brightness.scale, endpoint: endpoint });
fromZigbee = [...fromZigbee, ...ext.fromZigbee];
toZigbee = [...toZigbee, ...ext.toZigbee];
if (min) {
ext = tuyaModernExtend.dpNumeric({ name: "min_brightness", dp: min.dp, type: min.type, scale: min.scale, endpoint: endpoint });
fromZigbee = [...fromZigbee, ...ext.fromZigbee];
toZigbee = [...toZigbee, ...ext.toZigbee];
}
if (max) {
ext = tuyaModernExtend.dpNumeric({ name: "max_brightness", dp: max.dp, type: max.type, scale: max.scale, endpoint: endpoint });
fromZigbee = [...fromZigbee, ...ext.fromZigbee];
toZigbee = [...toZigbee, ...ext.toZigbee];
}
if (colorTemp) {
ext = tuyaModernExtend.dpNumeric({
name: "color_temp",
dp: colorTemp.dp,
type: colorTemp.type,
scale: colorTemp.scale,
endpoint: endpoint,
});
fromZigbee = [...fromZigbee, ...ext.fromZigbee];
toZigbee = [...toZigbee, ...ext.toZigbee];
}
// if (color) {
// const handlers = getHandlersForDP('color', color.dp, color.type,
// valueConverterBasic.color1000(), undefined, undefined, endpoint);
// fromZigbee = [...fromZigbee, ...handlers[0]];
// toZigbee = [...toZigbee, ...handlers[1]];
// }
// combine extends for one expose
return { exposes: [exp], fromZigbee, toZigbee, isModernExtend: true };
},
dpTemperature(args) {
return tuyaModernExtend.dpNumeric({ name: "temperature", type: exports.dataTypes.number, readOnly: true, scale: 10, expose: e.temperature(), ...args });
},
dpHumidity(args) {
return tuyaModernExtend.dpNumeric({ name: "humidity", type: exports.dataTypes.number, readOnly: true, expose: e.humidity(), ...args });
},
dpBattery(args) {
return tuyaModernExtend.dpNumeric({ name: "battery", type: exports.dataTypes.number, readOnly: true, expose: e.battery(), ...args });
},
dpBatteryState(args) {
return tuyaModernExtend.dpEnumLookup({
name: "battery_state",
type: exports.dataTypes.number,
lookup: { low: 0, medium: 1, high: 2 },
readOnly: true,
expose: tuyaExposes.batteryState(),
...args,
});
},
dpTemperatureUnit(args) {
return tuyaModernExtend.dpEnumLookup({
name: "temperature_unit",
type: exports.dataTypes.enum,
lookup: { celsius: 0, fahrenheit: 1 },
readOnly: true,
expose: tuyaExposes.temperatureUnit(),
...args,
});
},
dpContact(args, invert) {
return tuyaModernExtend.dpBinary({
name: "contact",
type: exports.dataTypes.bool,
valueOn: invert ? [true, true] : [true, false],
valueOff: invert ? [false, false] : [false, true],
readOnly: true,
expose: e.contact(),
...args,
});
},
dpAction(args) {
const { lookup } = args;
return tuyaModernExtend.dpEnumLookup({
name: "action",
type: exports.dataTypes.number,
readOnly: true,
expose: e.action(Object.keys(lookup)),
...args,
});
},
dpIlluminance(args) {
return tuyaModernExtend.dpNumeric({ name: "illuminance", type: exports.dataTypes.number, readOnly: true, expose: e.illuminance(), ...args });
},
dpGas(args, invert) {
return tuyaModernExtend.dpBinary({
name: "gas",
type: exports.dataTypes.enum,
valueOn: invert ? [true, 1] : [true, 0],
valueOff: invert ? [false, 0] : [false, 1],
readOnly: true,
expose: e.gas(),
...args,
});
},
dpOnOff(args) {
const { readOnly } = args;
return tuyaModernExtend.dpBinary({
name: "state",
type: exports.dataTypes.bool,
valueOn: ["ON", true],
valueOff: ["OFF", false],
expose: e.switch().setAccess("state", readOnly ? ea.STATE : ea.STATE_SET),
...args,
});
},
dpPowerOnBehavior(args) {
const { readOnly } = args;
let { lookup } = args;
lookup = lookup || { off: 0, on: 1, previous: 2 };
return tuyaModernExtend.dpEnumLookup({
name: "power_on_behavior",
lookup: lookup,
type: exports.dataTypes.enum,
expose: e.power_on_behavior(Object.keys(lookup)).withAccess(readOnly ? ea.STATE : ea.STATE_SET),
...args,
});
},
tuyaLight(args) {
args = { minBrightness: "none", powerOnBehavior: false, switchType: false, doNotDisturb: true, colorPowerOnBehavior: true, ...args };
if (args.colorTemp) {
args.colorTemp = { startup: false, ...args.colorTemp };
}
if (args.color) {
args.color = { applyRedFix: true, enhancedHue: false, ...(utils.isBoolean(args.color) ? {} : args.color) };
}
const result = modernExtend.light({ ...args, powerOnBehavior: false });
result.fromZigbee.push(tuyaFz.brightness);
if (args.doNotDisturb) {
result.toZigbee.push(tuyaTz.do_not_disturb);
result.exposes.push(tuyaExposes.doNotDisturb());
}
if (args.powerOnBehavior) {
result.fromZigbee.push(tuyaFz.power_on_behavior_2);
result.toZigbee.push(tuyaTz.power_on_behavior_2);
if (args.endpointNames) {
result.exposes.push(...args.endpointNames.map((ee) => e.power_on_behavior().withEndpoint(ee)));
}
else {
result.exposes.push(e.power_on_behavior());
}
}
if (args.switchType) {
result.fromZigbee.push(tuyaFz.switch_type);
result.toZigbee.push(tuyaTz.switch_type);
result.exposes.push(tuyaExposes.switchType());
}
if (args.minBrightness === "attribute") {
result.fromZigbee.push(tuyaFz.min_brightness_attribute);
result.toZigbee.push(tuyaTz.min_brightness_attribute);
result.exposes = result.exposes.map((e) => (typeof e !== "function" && utils.isLightExpose(e) ? e.withMinBrightness() : e));
}
else if (args.minBrightness === "command") {
result.toZigbee.push(tuyaTz.min_brightness_command);
result.exposes = result.exposes.map((e) => typeof e !== "function" && utils.isLightExpose(e) ? e.withMinBrightness().setAccess("min_brightness", ea.STATE_SET) : e);
}
if (args.color && args.colorPowerOnBehavior) {
result.toZigbee.push(tuyaTz.color_power_on_behavior);
result.exposes.push(tuyaExposes.colorPowerOnBehavior());
}
const tuyaLightingColorCtrl = tuyaClusters.addTuyaLightingColorCtrlCluster();
result.onEvent = [...(tuyaLightingColorCtrl.onEvent ?? []), ...(result.onEvent ?? [])];
result.configure = [...(tuyaLightingColorCtrl.configure ?? []), ...(result.configure ?? [])];
const customCluster3 = tuyaClusters.addManuSpecificTuya3Cluster();
result.onEvent = [...(customCluster3.onEvent ?? []), ...(result.onEvent ?? [])];
result.configure = [...(customCluster3.configure ?? []), ...(result.configure ?? [])];
result.configure.push((0, utils_1.configureSetPowerSourceWhenUnknown)("Mains (single phase)"));
return result;
},
tuyaOnOff: (args = {}) => {
const { onOffCountdown = false, indicatorMode = false, powerOutageMemory = false, childLock = false, inchingSwitch = false, backlightModeOffOn = false, powerOnBehavior2 = false, powerOnBehavior3 = false, switchType = false, } = args;
const exposes = args.endpoints
? args.endpoints.map((ee) => e.switch().withEndpoint(ee))
: [e.switch()];
// biome-ignore lint/suspicious/noExplicitAny: generic
const fromZigbee = [fz.on_off];
const toZigbee = [];
if (onOffCountdown) {
fromZigbee.push(tuyaFz.on_off_countdown);
toZigbee.push(tuyaTz.on_off_countdown);
if (typeof onOffCountdown === "function") {
exposes.push((d) => (onOffCountdown(d.manufacturerName) ? [tuyaExposes.countdown()] : []));
}
else if (args.endpoints) {
exposes.push(...args.endpoints.map((ee) => tuyaExposes.countdown().withAccess(ea.ALL).withEndpoint(ee)));
}
else {
exposes.push(tuyaExposes.countdown().withAccess(ea.ALL));
}
}
else {
toZigbee.push(tz.on_off);
}
if (powerOutageMemory) {
// Legacy, powerOnBehavior is preferred
fromZigbee.push(tuyaFz.power_outage_memory);
toZigbee.push(tuyaTz.power_on_behavior_1);
if (typeof powerOutageMemory === "function") {
exposes.push((d) => (powerOutageMemory(d.manufacturerName) ? [tuyaExposes.powerOutageMemory()] : []));
}
else {
exposes.push(tuyaExposes.powerOutageMemory());
}
}
else if (powerOnBehavior2) {
fromZigbee.push(tuyaFz.power_on_behavior_2);
toZigbee.push(tuyaTz.power_on_behavior_2);
const expose = args.endpoints ? args.endpoints.map((ee) => e.power_on_behavior().withEndpoint(ee)) : [e.power_on_behavior()];
if (typeof powerOnBehavior2 === "function") {
exposes.push((d) => (powerOnBehavior2(d.manufacturerName) ? expose : []));
}
else {
exposes.push(...expose);
}
}
else if (powerOnBehavior3) {
const endpointList = args.endpoints || [];
if (endpointList.length > 0) {
for (const endpoint of endpointList) {
const result = modernExtend.enumLookup({
name: "power_on_behavior",
lookup: { off: 0, on: 1, previous: 2 },
cluster: "manuSpecificTuya",
attribute: { ID: 0x4002, type: 0x30 },
description: "Controls the behavior when the device is powered on after power loss",
entityCategory: "config",
endpointName: endpoint,
});
fromZigbee.push(...result.fromZigbee);
toZigbee.push(...result.toZigbee);
exposes.push(...result.exposes);
}
}
else {
const result = modernExtend.enumLookup({
name: "power_on_behavior",
lookup: { off: 0, on: 1, previous: 2 },
cluster: "manuSpecificTuya",
attribute: { ID: 0x4002, type: 0x30 },
description: "Controls the behavior when the device is powered on after power loss",
entityCategory: "config",
});
fromZigbee.push(...result.fromZigbee);
toZigbee.push(...result.toZigbee);
exposes.push(...result.exposes);
}
}
else {
fromZigbee.push(tuyaFz.power_on_behavior_1);
toZigbee.push(tuyaTz.power_on_behavior_1);
exposes.push(e.power_on_behavior());
}
if (switchType) {
fromZigbee.push(tuyaFz.switch_type);
toZigbee.push(tuyaTz.switch_type);
if (typeof switchType === "function") {
exposes.push((d) => (switchType(d.manufacturerName) ? [tuyaExposes.switchType()] : []));
}
else {
exposes.push(tuyaExposes.switchType());
}
}
if (args.switchTypeCurtain) {
fromZigbee.push(tuyaFz.switch_type_curtain);
toZigbee.push(tuyaTz.switch_type_curtain);
exposes.push(tuyaExposes.switchTypeCurtain());
}
if (args.switchTypeButton) {
fromZigbee.push(tuyaFz.switch_type_button);
toZigbee.push(tuyaTz.switch_type_button);
exposes.push(tuyaExposes.switchTypeButton());
}
if (backlightModeOffOn) {
fromZigbee.push(tuyaFz.backlight_mode_off_on);
toZigbee.push(tuyaTz.backlight_indicator_mode_2);
if (typeof backlightModeOffOn === "function") {
exposes.push((d) => (backlightModeOffOn(d.manufacturerName) ? [tuyaExposes.backlightModeOffOn()] : []));
}
else {
exposes.push(tuyaExposes.backlightModeOffOn());
}
}
if (args.backlightModeLowMediumHigh) {
fromZigbee.push(tuyaFz.backlight_mode_low_medium_high);
exposes.push(tuyaExposes.backlightModeLowMediumHigh());
toZigbee.push(tuyaTz.backlight_indicator_mode_1);
}
if (args.backlightModeOffNormalInverted) {
fromZigbee.push(tuyaFz.backlight_mode_off_normal_inverted);
exposes.push(tuyaExposes.backlightModeOffNormalInverted());
toZigbee.push(tuyaTz.backlight_indicator_mode_1);
}
if (indicatorMode) {
fromZigbee.push(tuyaFz.indicator_mode);
toZigbee.push(tuyaTz.backlight_indicator_mode_1);
if (typeof indicatorMode === "function") {
exposes.push((d) => (indicatorMode(d.manufacturerName) ? [tuyaExposes.indicatorMode()] : []));
}
else {
exposes.push(tuyaExposes.indicatorMode());
}
}
if (args.indicatorModeNoneRelayPos) {
fromZigbee.push(tuyaFz.indicator_mode_none_relay_pos);
exposes.push(tuyaExposes.indicatorModeNoneRelayPos());
toZigbee.push(tuyaTz.backlight_indicator_mode_none_relay_pos);
}
if (args.electricalMeasurements) {
fromZigbee.push(args.electricalMeasurementsFzConverter || fz.electrical_measurement, fz.metering);
exposes.push(e.power(), e.current(), e.voltage(), e.energy());
}
if (childLock) {
fromZigbee.push(tuyaFz.child_lock);
toZigbee.push(tuyaTz.child_lock);
if (typeof childLock === "function") {
exposes.push((d) => (childLock(d.manufacturerName) ? [e.child_lock()] : []));
}
else {
exposes.push(e.child_lock());
}
}
if (args.switchMode) {
if (args.endpoints) {
args.endpoints.forEach((ep) => {
const epExtend = tuyaModernExtend.tuyaSwitchMode({
description: `Switch mode ${ep}`,
endpointName: ep,
});
fromZigbee.push(...epExtend.fromZigbee);
toZigbee.push(...epExtend.toZigbee);
exposes.push(...epExtend.exposes);
});
}
else {
const extend = tuyaModernExtend.tuyaSwitchMode({ description: "Switch mode" });
fromZigbee.push(...extend.fromZigbee);
toZigbee.push(...extend.toZigbee);
exposes.push(...extend.exposes);
}
}
if (inchingSwitch) {
const quantity = args.endpoints?.length ?? 1;
fromZigbee.push(tuyaFz.inchingSwitch);
toZigbee.push(tuyaTz.inchingSwitch);
if (typeof inchingSwitch === "function") {
exposes.push((d) => (inchingSwitch(d.manufacturerName) ? [tuyaExposes.inchingSwitch(quantity)] : []));
}
else {
exposes.push(tuyaExposes.inchingSwitch(quantity));
}
}
const configure = [(0, utils_1.configureSetPowerSourceWhenUnknown)("Mains (single phase)")];
return { exposes, fromZigbee, toZigbee, isModernExtend: true, configure };
},
dpBacklightMode(args) {
const { readOnly } = args;
let { lookup } = args;
lookup = lookup || { off: 0, normal: 1, inverted: 2 };
return tuyaModernExtend.dpEnumLookup({
name: "backlight_mode",
lookup: lookup,
type: exports.dataTypes.enum,
expose: tuyaExposes.backlightModeOffNormalInverted().withAccess(readOnly ? ea.STATE : ea.STATE_SET),
...args,
});
},
combineActions(actions) {
let newValues = [];
// biome-ignore lint/suspicious/noExplicitAny: too messy
let newFromZigbee = [];
let description;
// collect action values and handlers
for (const actionME of actions) {
const { exposes, fromZigbee } = actionME;
newValues = newValues.concat(exposes[0].values);
description = exposes[0].description;
newFromZigbee = newFromZigbee.concat(fromZigbee);
}
// create single enum-expose
const exp = new exposes.Enum("action", ea.STATE, newValues).withDescription(description);
return { exposes: [exp], fromZigbee: newFromZigbee, isModernExtend: true };
},
tuyaCoverSwitchType: (args) => modernExtend.enumLookup({
name: "switch_type",
lookup: { momentary: 0, toggle: 1 },
cluster: "closuresWindowCovering",
attribute: "tuyaSwitchType",
description: "Type of the installed switch",
entityCategory: "config",
...args,
}),
tuyaSwitchMode: (args) => modernExtend.enumLookup({
name: "switch_mode",
lookup: { switch: 0, scene: 1 },
cluster: "manuSpecificTuya3",
attribute: "switchMode",
description: "Work mode for switch",
entityCategory: "config",
...args,
}),
tuyaLedIndicator() {
const fromZigbee = [tuyaFz.backlight_mode_off_normal_inverted];
const exp = tuyaExposes.backlightModeOffNormalInverted();
const toZigbee = [tuyaTz.backlight_indicator_mode_1];
return { exposes: [exp], toZigbee, fromZigbee, isModernExtend: true };
},
tuyaMagicPacket() {
return { configure: [exports.configureMagicPacket], isModernExtend: true };
},
tuyaOnOffAction(args) {
return modernExtend.actionEnumLookup({
actionLookup: { 0: "single", 1: "double", 2: "hold" },
cluster: "genOnOff",
commands: ["commandTuyaAction"],
attribute: "value",
});
},
tuyaOnOffActionLegacy(args) {
// For new devices use tuyaOnOffAction instead
const actions = args.actions.flatMap((a) => (args.endpointNames ? args.endpointNames.map((e) => `${e}_${a}`) : [a]));
const exposes = [e.action(actions)];
const fromZigbee = [tuyaFz.on_off_action];
return { exposes, fromZigbee, isModernExtend: true };
},
dpChildLock(args) {
return tuyaModernExtend.dpBinary({
name: "child_lock",
type: exports.dataTypes.bool,
valueOn: ["LOCK", true],
valueOff: ["UNLOCK", false],
expose: e.child_lock(),
...args,
});
},
tuyaWeatherForecast(args = {}) {
const { includeCurrentWeather = true, numberOfForecastDays = 3, correctForNegativeValues = false, weatherConditionMap = exports.M8ProTuyaWeatherCondition, } = args;
const tz_fileds = includeCurrentWeather ? ["temperature_0", "humidity_0", "condition_0"] : [];
for (let i = 0; i < numberOfForecastDays; ++i) {
tz_fileds.push(`temperature_${i}`);
tz_fileds.push(`humidity_${i}`);
tz_fileds.push(`condition_${i}`);
}
function _vCorr(val) {
if (correctForNegativeValues && val < 1) {
return val - 1;
}
return val;
}
function _prepareTuyaWeatherSyncPayload(meta, numberOfForecastDays, includeCurrentWeather) {
let bOffset = 0;
const buffer = Buffer.alloc(6 + (includeCurrentWeather ? 6 : 1) + numberOfForecastDays * 6);
buffer.writeUInt8(0x11, bOffset++);
buffer.writeUInt8(0, bOffset++);
buffer.writeUInt8(0x12, bOffset++);
buffer.writeUInt8(numberOfForecastDays, bOffset++);
buffer.writeUInt8(0x13, bOffset++);
const weather_values = { 1: [], 2: [], 3: [] };
if (includeCurrentWeather) {
buffer.writeUInt8(0x1, bOffset++);
weather_values[TuyaWeatherID.Temperature].push("temperature_0" in meta.state ? _vCorr(meta.state["temperature_0"]) : 0);
weather_values[TuyaWeatherID.Humidity].push("humidity_0" in meta.state ? meta.state["humidity_0"] : 0);
weather_values[TuyaWeatherID.Condition].push("condition_0" in meta.state ? weatherConditionMap[meta.state["condition_0"]] : 0);
}
else {
buffer.writeUInt8(0x0, bOffset++);
}
for (let i = 1; i <= numberOfForecastDays; ++i) {
weather_values[TuyaWeatherID.Temperature].push(`temperature_${i}` in meta.state ? _vCorr(meta.state[`temperature_${i}`]) : 0);
weather_values[TuyaWeatherID.Humidity].push(`humidity_${i}` in meta.state ? meta.state[`humidity${i}`] : 0);
weather_values[TuyaWeatherID.Condition].push(`condition_${i}` in meta.state ? weatherConditionMap[meta.state[`condition_${i}`]] : 0);
}
for (const id of [1, 2, 3]) {
buffer.writeUInt8(id, bOffset++);
for (const j of weather_values[id]) {
if (id === TuyaWeatherID.Temperature) {
buffer.writeInt16BE(j, bOffset);
bOffset += 2;
}
else if (id === TuyaWeatherID.Humidity) {
buffer.writeInt16BE(j, bOffset);
bOffset += 2;
}
else if (id === TuyaWeatherID.Condition) {
buffer.writeUInt8(j, bOffset++);
}
}
}
buffer.writeUInt8(0, bOffset++);
return buffer;
}
const fzConverter = {
type: ["commandTuyaWeatherRequest"],
cluster: "manuSpecificTuya",
convert: (model, msg, publish, options, meta) => {
if (msg.type === "commandTuyaWeatherRequest") {
// Although the parameter is specified as "Data length" in the documentation, some devices
// send values in this field that don't correspond to the rest of packet data. Relying on this
// field would lead to parsing errors in those cases, thus it's required to search for constant flags.
let bOffset = 0;
const buffer = msg.data.payload;
const _length = buffer.readUInt16LE(bOffset);
bOffset += 2;
const _version_number = buffer.readUInt8(bOffset++);
const _location_type = buffer.readUInt8(bOffset++);
const weather_request = [];
let nextField = buffer.readUInt8(bOffset++);
while (nextField !== 0x12 && nextField !== 0x13) {
weather_request.push(nextField);
nextField = buffer.readUInt8(bOffset++);
}
const number_of_forecast_days = nextField === 0x12 ? buffer.readUInt8(bOffset++) : 0;
const _current_weather_flag = nextField === 0x12 ? buffer.readUInt8(bOffset++) : 0;
const include_current_weather = buffer.readUInt8(bOffset++) !== 0;
const pld = _prepareTuyaWeatherSyncPayload(meta, number_of_forecast_days, include_current_weather);
msg.endpoint
.command("manuSpecificTuya", "tuyaWeatherSync", { payload: pld })
.catch((error) => logger_1.logger.warning(() => `Failed to sync '${msg.device.ieeeAddr}:${msg.endpoint.ID}' on weather request (${error})`, NS));
}
},
};
const tzConverter = {
key: tz_fileds,
convertSet: (entity, key, value, meta) => {
meta.state[key] = value;
const pld = _prepareTuyaWeatherSyncPayload(meta, numberOfForecastDays, includeCurrentWeather);
entity
.command("manuSpecificTuya", "tuyaWeatherSync", { payload: pld })
.catch((error) => () => logger_1.logger.warning(`Failed to sync weather for '${utils.isGroup(entity) ? entity.groupID : entity.ID}' (${error})`, NS));
return { state: { [key]: value } };
},
};
const result = {
configure: [],
isModernExtend: true,
fromZigbee: [fzConverter],
toZigbee: [tzConverter],
};
return result;
},
};
exports.modernExtend = tuyaModernExtend;
const tuyaClusters = {
addTuyaClosuresWindowCoveringCluster: () => modernExtend.deviceAddCustomCluster("closuresWindowCovering", {
name: "closuresWindowCovering",
ID: zigbee_herdsman_1.Zcl.Clusters.closuresWindowCovering.ID,
attributes: {
tuyaMovingState: { name: "tuyaMovingState", ID: 0xf000, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
tuyaCalibration: { name: "tuyaCalibration", ID: 0xf001, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
tuyaMotorReversal: { name: "tuyaMotorReversal", ID: 0xf002, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
moesCalibrationTime: { name: "moesCalibrationTime", ID: 0xf003, type: zigbee_herdsman_1.Zcl.DataType.UINT16, write: true, max: 0xffff },
tuyaSwitchType: { name: "tuyaSwitchType", ID: 0x8000, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
},
commands: {},
commandsResponse: {},
}),
addTuyaGenBasicCluster: () => modernExtend.deviceAddCustomCluster("genBasic", {
name: "genBasic",
ID: zigbee_herdsman_1.Zcl.Clusters.genBasic.ID,
attributes: {},
commands: {
tuyaSetup: { name: "tuyaSetup", ID: 0xf0, parameters: [] },
},
commandsResponse: {},
}),
addTuyaGenGroupsCluster: () => modernExtend.deviceAddCustomCluster("genGroups", {
name: "genGroups",
ID: zigbee_herdsman_1.Zcl.Clusters.genGroups.ID,
attributes: {},
commands: {
miboxerSetZones: { name: "miboxerSetZones", ID: 0xf0, parameters: [{ name: "zones", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.LIST_MIBOXER_ZONES }] },
},
commandsResponse: {},
}),
addTuyaGenOnOffCluster: () => modernExtend.deviceAddCustomCluster("genOnOff", {
name: "genOnOff",
ID: zigbee_herdsman_1.Zcl.Clusters.genOnOff.ID,
attributes: {
tuyaBacklightSwitch: { name: "tuyaBacklightSwitch", ID: 0x5000, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
tuyaBacklightMode: { name: "tuyaBacklightMode", ID: 0x8001, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
moesStartUpOnOff: { name: "moesStartUpOnOff", ID: 0x8002, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
tuyaOperationMode: { name: "tuyaOperationMode", ID: 0x8004, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
},
commands: {
tuyaCountdown: {
name: "tuyaCountdown",
ID: 0xf0,
parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }],
},
tuyaAction2: { name: "tuyaAction2", ID: 0xfc, parameters: [{ name: "value", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff }] },
tuyaAction: {
name: "tuyaAction",
ID: 0xfd,
parameters: [
{ name: "value", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER },
],
},
},
commandsResponse: {},
}),
addTuyaGenLevelCtrlCluster: () => modernExtend.deviceAddCustomCluster("genLevelCtrl", {
name: "genLevelCtrl",
ID: zigbee_herdsman_1.Zcl.Clusters.genLevelCtrl.ID,
attributes: {},
commands: {
moveToLevelTuya: {
name: "moveToLevelTuya",
ID: 0xf0,
parameters: [
{ name: "level", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "transtime", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
],
},
},
commandsResponse: {},
}),
addTuyaLightingColorCtrlCluster: () => modernExtend.deviceAddCustomCluster("lightingColorCtrl", {
name: "lightingColorCtrl",
ID: zigbee_herdsman_1.Zcl.Clusters.lightingColorCtrl.ID,
attributes: {
tuyaRgbMode: { name: "tuyaRgbMode", ID: 0xf000, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff },
tuyaBrightness: { name: "tuyaBrightness", ID: 0xf001, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff },
},
commands: {
tuyaMoveToHueAndSaturationBrightness: {
name: "tuyaMoveToHueAndSaturationBrightness",
ID: 0x06,
parameters: [
{ name: "hue", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "saturation", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "transtime", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "brightness", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
],
},
tuyaSetMinimumBrightness: {
name: "tuyaSetMinimumBrightness",
ID: 0xe0,
parameters: [{ name: "minimum", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff }],
},
tuyaMoveToHueAndSaturationBrightness2: {
name: "tuyaMoveToHueAndSaturationBrightness2",
ID: 0xe1,
parameters: [
{ name: "hue", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "saturation", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "brightness", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
],
},
tuyaRgbMode: { name: "tuyaRgbMode", ID: 0xf0, parameters: [{ name: "enable", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff }] },
tuyaOnStartUp: {
name: "tuyaOnStartUp",
ID: 0xf9,
parameters: [
{ name: "mode", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.LIST_UINT8 },
],
},
tuyaDoNotDisturb: { name: "tuyaDoNotDisturb", ID: 0xfa, parameters: [{ name: "enable", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff }] },
tuyaOnOffTransitionTime: {
name: "tuyaOnOffTransitionTime",
ID: 0xfb,
parameters: [
{ name: "unknown", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "onTransitionTime", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BIG_ENDIAN_UINT24 },
{ name: "offTransitionTime", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BIG_ENDIAN_UINT24 },
],
},
},
commandsResponse: {},
}),
addManuSpecificTuya2Cluster: () => modernExtend.deviceAddCustomCluster("manuSpecificTuya2", {
name: "manuSpecificTuya2",
ID: 0xe002,
attributes: {
alarmTemperatureMax: { name: "alarmTemperatureMax", ID: 0xd00a, type: zigbee_herdsman_1.Zcl.DataType.INT16, write: true, min: -32768, max: 32767 },
alarmTemperatureMin: { name: "alarmTemperatureMin", ID: 0xd00b, type: zigbee_herdsman_1.Zcl.DataType.INT16, write: true, min: -32768, max: 32767 },
alarmHumidityMax: { name: "alarmHumidityMax", ID: 0xd00d, type: zigbee_herdsman_1.Zcl.DataType.INT16, write: true, min: -32768, max: 32767 },
alarmHumidityMin: { name: "alarmHumidityMin", ID: 0xd00e, type: zigbee_herdsman_1.Zcl.DataType.INT16, write: true, min: -32768, max: 32767 },
alarmHumidity: { name: "alarmHumidity", ID: 0xd00f, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
alarmTemperature: { name: "alarmTemperature", ID: 0xd006, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
unknown: { name: "unknown", ID: 0xd010, type: zigbee_herdsman_1.Zcl.DataType.UINT8, write: true, max: 0xff },
},
commands: {},
commandsResponse: {},
}),
addManuSpecificTuya3Cluster: () => modernExtend.deviceAddCustomCluster("manuSpecificTuya3", {
name: "manuSpecificTuya3",
ID: 0xe001,
attributes: {
powerOnBehavior: { name: "powerOnBehavior", ID: 0xd010, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
switchMode: { name: "switchMode", ID: 0xd020, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
switchType: { name: "switchType", ID: 0xd030, type: zigbee_herdsman_1.Zcl.DataType.ENUM8, write: true, max: 0xff },
},
commands: {
setOptions1: { name: "setOptions1", ID: 0xe5, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }] },
setOptions2: { name: "setOptions2", ID: 0xe6, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }] },
setOptions3: { name: "setOptions3", ID: 0xe7, parameters: [{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }] },
},
commandsResponse: {},
}),
addTuyaCommonPrivateCluster: () => modernExtend.deviceAddCustomCluster("manuSpecificTuya4", {
name: "manuSpecificTuya4",
ID: 0xe000,
attributes: {
random_timing: { name: "random_timing", ID: 0xd001, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
cycle_timing: { name: "cycle_timing", ID: 0xd002, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
inching: { name: "inching", ID: 0xd003, type: zigbee_herdsman_1.Zcl.DataType.CHAR_STR, write: true },
},
commands: {
setRandomTiming: {
name: "setRandomTiming",
ID: 0xf7,
parameters: [{ name: "payload", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }],
},
setCycleTiming: {
name: "setCycleTiming",
ID: 0xf8,
parameters: [{ name: "payload", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }],
},
setInchingSwitch: {
name: "setInchingSwitch",
ID: 0xfb,
parameters: [{ name: "payload", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER }],
},
},
commandsResponse: {},
}),
};
exports.clusters = tuyaClusters;
//# sourceMappingURL=tuya.js.map