zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
5,283 lines • 251 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.toZigbee = exports.fromZigbee = exports.modernExtend = exports.lumiModernExtend = exports.manufacturerCode = exports.trv = exports.presence = exports.numericAttributes2Payload = exports.buffer2DataObject = void 0;
const node_buffer_1 = require("node:buffer");
const fz = __importStar(require("../converters/fromZigbee"));
const exposes = __importStar(require("./exposes"));
const logger_1 = require("./logger");
const modernExtend = __importStar(require("./modernExtend"));
const globalStore = __importStar(require("./store"));
const utils_1 = require("./utils");
const NS = "zhc:lumi";
const e = exposes.presets;
const ea = exposes.access;
const buffer2DataObject = (model, buffer) => {
const dataObject = {};
if (buffer !== null && node_buffer_1.Buffer.isBuffer(buffer)) {
// Lumi struct parsing
for (let i = 0; i < buffer.length - 1; i++) {
const index = buffer[i];
let value = null;
switch (buffer[i + 1]) {
case 16:
case 32:
// 0x10 ZclBoolean
// 0x20 Zcl8BitUint
value = buffer.readUInt8(i + 2);
i += 2;
break;
case 33:
// 0x21 Zcl16BitUint
value = buffer.readUInt16LE(i + 2);
i += 3;
break;
case 34:
// 0x22 Zcl24BitUint
value = buffer.readUIntLE(i + 2, 3);
i += 4;
break;
case 35:
// 0x23 Zcl32BitUint
value = buffer.readUInt32LE(i + 2);
i += 5;
break;
case 36:
// 0x24 Zcl40BitUint
value = buffer.readUIntLE(i + 2, 5);
i += 6;
break;
case 37:
// 0x25 Zcl48BitUint
value = buffer.readUIntLE(i + 2, 6);
i += 7;
break;
case 38:
// 0x26 Zcl56BitUint
value = buffer.readUIntLE(i + 2, 7);
i += 8;
break;
case 39:
// 0x27 Zcl64BitUint
value = buffer.readBigUInt64BE(i + 2);
i += 9;
break;
case 40:
// 0x28 Zcl8BitInt
value = buffer.readInt8(i + 2);
i += 2;
break;
case 41:
// 0x29 Zcl16BitInt
value = buffer.readInt16LE(i + 2);
i += 3;
break;
case 42:
// 0x2A Zcl24BitInt
value = buffer.readIntLE(i + 2, 3);
i += 4;
break;
case 43:
// 0x2B Zcl32BitInt
value = buffer.readInt32LE(i + 2);
i += 5;
break;
case 44:
// 0x2C Zcl40BitInt
value = buffer.readIntLE(i + 2, 5);
i += 6;
break;
case 45:
// 0x2D Zcl48BitInt
value = buffer.readIntLE(i + 2, 6);
i += 7;
break;
case 46:
// 0x2E Zcl56BitInt
value = buffer.readIntLE(i + 2, 7);
i += 8;
break;
case 47:
// 0x2F Zcl64BitInt
value = buffer.readBigInt64BE(i + 2);
i += 9;
break;
case 57:
// 0x39 ZclSingleFloat
value = buffer.readFloatLE(i + 2);
i += 5;
break;
case 58:
// 0x3a ZclDoubleFloat
value = buffer.readDoubleLE(i + 2);
i += 5;
break;
case 66:
// 0x42 unknown, length taken from what seems correct in the logs, maybe is wrong
logger_1.logger.debug(`${model.model}: unknown vtype=${buffer[i + 1]}, pos=${i + 1}, moving length 1`, NS);
i += 2;
break;
case 95:
// 0x5f unknown, length taken from what seems correct in the logs, maybe is wrong
logger_1.logger.debug(`${model.model}: unknown vtype=${buffer[i + 1]}, pos=${i + 1}, moving length 4`, NS);
i += 5;
break;
default:
logger_1.logger.debug(`${model.model}: unknown vtype=${buffer[i + 1]}, pos=${i + 1}`, NS);
}
if (value != null) {
dataObject[index] = value;
}
}
}
logger_1.logger.debug(`${model.model}: Processed buffer into data \
${JSON.stringify(dataObject, (key, value) => (typeof value === "bigint" ? value.toString() : value))}`, NS);
return dataObject;
};
exports.buffer2DataObject = buffer2DataObject;
const numericAttributes2Payload = async (msg, meta, model, options, dataObject) => {
let payload = {};
for (const [key, value] of Object.entries(dataObject)) {
switch (key) {
case "0":
payload.detection_period = value;
break;
case "1":
payload.voltage = value;
if (model.meta?.battery?.voltageToPercentage) {
(0, utils_1.assertNumber)(value);
payload.battery = (0, utils_1.batteryVoltageToPercentage)(value, model.meta.battery.voltageToPercentage);
}
break;
case "2":
if (["JT-BZ-01AQ/A"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
payload.power_outage_count = value - 1;
}
break;
case "3":
if (["WXCJKG11LM", "WXCJKG12LM", "WXCJKG13LM", "MCCGQ14LM", "GZCGQ01LM", "JY-GZ-01AQ", "CTP-R01"].includes(model.model)) {
// The temperature value is constant 25 °C and does not change, so we ignore it
// https://github.com/Koenkk/zigbee2mqtt/issues/11126
// https://github.com/Koenkk/zigbee-herdsman-converters/pull/3585
// https://github.com/Koenkk/zigbee2mqtt/issues/13253
}
else {
(0, utils_1.assertNumber)(value);
payload.device_temperature = value; // 0x03
}
break;
case "4":
if ([
"WS-USC01",
"WS-USC02",
"WS-EUK01",
"WS-EUK02",
"QBKG27LM",
"QBKG28LM",
"QBKG29LM",
"QBKG25LM",
"QBKG38LM",
"QBKG39LM",
"ZNQBKG42LM",
"ZNQBKG43LM",
"ZNQBKG44LM",
"ZNQBKG45LM",
].includes(model.model)) {
payload.mode_switch = (0, utils_1.getFromLookup)(value, { 4: "anti_flicker_mode", 1: "quick_mode" });
}
break;
case "5":
(0, utils_1.assertNumber)(value);
payload.power_outage_count = value - 1;
break;
case "6":
if (["MCCGQ11LM", "SJCGQ11LM"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
let count = value;
// Sometimes, especially when the device is connected through another lumi router, the sensor
// send random values after 16 bit (>65536), so we truncate and read this as 16BitUInt.
count = Number.parseInt(count.toString(16).slice(-4), 16);
payload.trigger_count = count - 1;
}
break;
case "8":
if (["ZNLDP13LM"].includes(model.model)) {
// We don't know what the value means for these devices.
}
break;
case "9":
if (["ZNLDP13LM", "ZNXDD01LM"].includes(model.model)) {
// We don't know what the value means for these devices.
}
break;
case "10":
// Value 29146 is received for SSM-U02 sometimes here:
// https://github.com/Koenkk/zigbee2mqtt/issues/17961#issuecomment-1616170548
if (["SSM-U01", "DLKZMK11LM", "SSM-U02", "DLKZMK12LM"].includes(model.model) && (value === 1 || value === 2)) {
payload.switch_type = (0, utils_1.getFromLookup)(value, { 1: "toggle", 2: "momentary" });
}
break;
case "11":
if (["RTCGQ11LM"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
payload.illuminance = value;
}
break;
case "12":
if (["ZNLDP13LM", "ZNXDD01LM"].includes(model.model)) {
// We don't know what the value means for these devices.
}
break;
case "13":
if (["ZNXDD01LM"].includes(model.model)) {
// We don't know what the value means for these devices.
}
else if (["ZNCLBL01LM"].includes(model.model)) {
// Overwrite version advertised by `genBasic` and `genOta` with correct version:
// https://github.com/Koenkk/zigbee2mqtt/issues/15745
(0, utils_1.assertNumber)(value);
meta.device.meta.lumiFileVersion = value;
meta.device.softwareBuildID = exports.trv.decodeFirmwareVersionString(value);
meta.device.save();
}
break;
case "17":
if (["ZNXDD01LM"].includes(model.model)) {
// We don't know what the value means for these devices.
}
break;
case "100":
if ([
"QBKG18LM",
"QBKG20LM",
"QBKG31LM",
"QBKG39LM",
"QBKG41LM",
"QBCZ15LM",
"LLKZMK11LM",
"QBKG12LM",
"QBKG03LM",
"QBKG25LM",
].includes(model.model)) {
// biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
let mapping;
switch (model.model) {
case "QBCZ15LM":
mapping = "relay";
break;
case "LLKZMK11LM":
mapping = "l1";
break;
default:
mapping = "left";
}
payload[`state_${mapping}`] = value === 1 ? "ON" : "OFF";
}
else if (["WXKG14LM", "WXKG16LM", "WXKG17LM"].includes(model.model)) {
payload.click_mode = (0, utils_1.getFromLookup)(value, { 1: "fast", 2: "multi" });
}
else if (["WXCJKG11LM", "WXCJKG12LM", "WXCJKG13LM", "ZNMS12LM", "ZNCLBL01LM", "RTCGQ12LM", "RTCGQ13LM", "RTCGQ14LM"].includes(model.model)) {
// We don't know what the value means for these devices.
// https://github.com/Koenkk/zigbee2mqtt/issues/11126
// https://github.com/Koenkk/zigbee2mqtt/issues/12279
}
else if (["RTCGQ15LM"].includes(model.model)) {
payload.occupancy = value;
}
else if (["WSDCGQ01LM", "WSDCGQ11LM", "WSDCGQ12LM", "VOCKQJK11LM"].includes(model.model)) {
// https://github.com/Koenkk/zigbee2mqtt/issues/798
// Sometimes the sensor publishes non-realistic vales, filter these
// @ts-expect-error ignore
const temperature = Number.parseFloat(value) / 100.0;
if (temperature > -65 && temperature < 65) {
payload.temperature = temperature;
}
}
else if (["RTCGQ11LM"].includes(model.model)) {
// It contains the occupancy, but in z2m we use a custom timer to do it, so we ignore it
// payload.occupancy = value === 1;
}
else if (["MCCGQ11LM", "MCCGQ14LM"].includes(model.model)) {
payload.contact = value === 0;
}
else if (["SJCGQ11LM"].includes(model.model)) {
// Ignore the message. It seems not reliable. See discussion here https://github.com/Koenkk/zigbee2mqtt/issues/12018
// payload.water_leak = value === 1;
}
else if (["SJCGQ13LM"].includes(model.model)) {
payload.water_leak = value === 1;
}
else if (["JTYJ-GD-01LM/BW"].includes(model.model)) {
payload.smoke_density = value;
}
else if (["GZCGQ01LM"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
payload.illuminance = value;
}
else {
payload.state = value === 1 ? "ON" : "OFF";
}
break;
case "101":
if ([
"QBKG18LM",
"QBKG20LM",
"QBKG31LM",
"QBKG39LM",
"QBKG41LM",
"QBCZ15LM",
"QBKG25LM",
"QBKG33LM",
"QBKG34LM",
"LLKZMK11LM",
"QBKG12LM",
"QBKG03LM",
].includes(model.model)) {
// biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
let mapping;
switch (model.model) {
case "QBCZ15LM":
mapping = "usb";
break;
case "QBKG25LM":
case "QBKG33LM":
case "QBKG34LM":
mapping = "center";
break;
case "LLKZMK11LM":
mapping = "l2";
break;
default:
mapping = "right";
}
payload[`state_${mapping}`] = value === 1 ? "ON" : "OFF";
}
else if (["RTCGQ12LM", "RTCGQ14LM", "RTCGQ15LM"].includes(model.model)) {
// Sometimes RTCGQ14LM reports high illuminance values in the dark
// https://github.com/Koenkk/zigbee2mqtt/issues/12596
(0, utils_1.assertNumber)(value);
const illuminance = value > 65000 ? 0 : value;
payload.illuminance = illuminance;
}
else if (["WSDCGQ01LM", "WSDCGQ11LM", "WSDCGQ12LM", "VOCKQJK11LM"].includes(model.model)) {
// https://github.com/Koenkk/zigbee2mqtt/issues/798
// Sometimes the sensor publishes non-realistic vales, filter these
// @ts-expect-error ignore
const humidity = Number.parseFloat(value) / 100.0;
if (humidity >= 0 && humidity <= 100) {
payload.humidity = humidity;
}
}
else if (["ZNJLBL01LM", "ZNCLDJ12LM"].includes(model.model)) {
payload.battery = value;
}
else if (["ZNCLBL01LM"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
const battery = value / 2;
payload.battery = (0, utils_1.precisionRound)(battery, 2);
}
else if (["RTCZCGQ11LM"].includes(model.model)) {
payload.presence = (0, utils_1.getFromLookup)(value, { 0: false, 1: true, 255: null });
}
else if (["ZNXDD01LM"].includes(model.model)) {
payload.brightness = value;
}
break;
case "102":
if (["QBKG25LM", "QBKG33LM", "QBKG34LM"].includes(model.model)) {
payload.state_right = value === 1 ? "ON" : "OFF";
}
else if (["WSDCGQ01LM", "WSDCGQ11LM"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
payload.pressure = value / 100.0;
}
else if (["WSDCGQ12LM"].includes(model.model)) {
// This pressure value is ignored because it is less accurate than reported in the 'scaledValue' attribute
// of the 'msPressureMeasurement' cluster
}
else if (["RTCZCGQ11LM"].includes(model.model)) {
if (meta.device.applicationVersion < 50) {
payload.presence_event = (0, utils_1.getFromLookup)(value, {
0: "enter",
1: "leave",
2: "left_enter",
3: "right_leave",
4: "right_enter",
5: "left_leave",
6: "approach",
7: "away",
255: null,
});
}
else {
payload.motion_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "low", 2: "medium", 3: "high" });
}
}
else if (["ZNXDD01LM"].includes(model.model)) {
payload.color_temp = value;
}
break;
case "103":
if (["RTCZCGQ11LM"].includes(model.model)) {
payload.monitoring_mode = (0, utils_1.getFromLookup)(value, { 0: "undirected", 1: "left_right" });
}
else if (["ZNXDD01LM"].includes(model.model)) {
// const color_temp_min = (value & 0xffff); // 2700
// const color_temp_max = (value >> 16) & 0xffff; // 6500
}
break;
case "105":
if (["RTCGQ13LM"].includes(model.model)) {
payload.motion_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "low", 2: "medium", 3: "high" });
}
else if (["RTCZCGQ11LM"].includes(model.model)) {
payload.approach_distance = (0, utils_1.getFromLookup)(value, { 0: "far", 1: "medium", 2: "near" });
}
else if (["RTCGQ14LM"].includes(model.model)) {
payload.detection_interval = value;
}
break;
case "106":
if (["RTCGQ14LM"].includes(model.model)) {
payload.motion_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "low", 2: "medium", 3: "high" });
}
break;
case "107":
if (["RTCGQ14LM"].includes(model.model)) {
payload.trigger_indicator = value === 1;
}
else if (["ZNCLBL01LM"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
const position = options.invert_cover ? 100 - value : value;
payload.position = position;
payload.state = options.invert_cover ? (position > 0 ? "CLOSE" : "OPEN") : position > 0 ? "OPEN" : "CLOSE";
}
break;
case "149":
(0, utils_1.assertNumber)(value);
payload.energy = value; // 0x95
if (["LLKZMK12LM"].includes(model.model)) {
(0, utils_1.assertNumber)(payload.energy);
payload.energy = payload.energy / 1000;
}
// Consumption is deprecated
payload.consumption = payload.energy;
break;
case "150":
if (["KD-R01D"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
payload.voltage = value * 0.01;
}
else if (!["JTYJ-GD-01LM/BW"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
payload.voltage = value * 0.1; // 0x96
}
break;
case "151":
if (["LLKZMK11LM"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
payload.current = value;
}
else {
(0, utils_1.assertNumber)(value);
payload.current = value * 0.001;
}
break;
case "152":
if (["DJT11LM"].includes(model.model)) {
// We don't know what implies for this device, it contains values like 30, 50,... that don't seem to change
}
else {
(0, utils_1.assertNumber)(value);
payload.power = value; // 0x98
}
break;
case "154":
if (["ZNLDP13LM", "ZNXDD01LM"].includes(model.model)) {
// We don't know what the value means for these devices.
}
break;
case "159":
if (["JT-BZ-01AQ/A"].includes(model.model)) {
payload.gas_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "15%LEL", 2: "10%LEL" });
}
else if (["MCCGQ13LM"].includes(model.model)) {
payload.detection_distance = (0, utils_1.getFromLookup)(value, { 1: "10mm", 2: "20mm", 3: "30mm" });
}
break;
case "160":
if (["JT-BZ-01AQ/A"].includes(model.model)) {
payload.gas = value === 1;
}
else if (["JY-GZ-01AQ"].includes(model.model)) {
payload.smoke = value === 1;
}
break;
case "161":
if (["JT-BZ-01AQ/A"].includes(model.model)) {
payload.gas_density = value;
}
else if (["JY-GZ-01AQ"].includes(model.model)) {
payload.smoke_density = value;
payload.smoke_density_dbm = (0, utils_1.getFromLookup)(value, {
0: 0,
1: 0.085,
2: 0.088,
3: 0.093,
4: 0.095,
5: 0.1,
6: 0.105,
7: 0.11,
8: 0.115,
9: 0.12,
10: 0.125,
});
}
break;
case "162":
if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
payload.test = value === 1;
}
break;
case "163":
if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
payload.buzzer_manual_mute = value === 1;
}
break;
case "164":
if (["JT-BZ-01AQ/A"].includes(model.model)) {
payload.state = (0, utils_1.getFromLookup)(value, { 0: "work", 1: "preparation" });
}
else if (["JY-GZ-01AQ"].includes(model.model)) {
payload.heartbeat_indicator = value === 1;
}
break;
case "165":
if (["JY-GZ-01AQ"].includes(model.model)) {
payload.linkage_alarm = value === 1;
}
break;
case "166":
if (["JT-BZ-01AQ/A"].includes(model.model)) {
payload.linkage_alarm = value === 1;
}
break;
case "238":
if (["ZNXDD01LM"].includes(model.model)) {
// We don't know what the value means for these devices.
}
else if (["ZNCLBL01LM"].includes(model.model)) {
// Overwrite version advertised by `genBasic` and `genOta` with correct version:
// https://github.com/Koenkk/zigbee2mqtt/issues/15745
(0, utils_1.assertNumber)(value);
meta.device.meta.lumiFileVersion = value;
meta.device.softwareBuildID = exports.trv.decodeFirmwareVersionString(value);
meta.device.save();
}
break;
case "240":
payload.flip_indicator_light = value === 1 ? "ON" : "OFF";
break;
case "247":
{
const dataObject247 = (0, exports.buffer2DataObject)(model, value);
if (["CTP-R01"].includes(model.model)) {
// execute pending soft switch of operation_mode, if exists
const opModeSwitchTask = globalStore.getValue(meta.device, "opModeSwitchTask");
if (opModeSwitchTask) {
const { callback, newMode } = opModeSwitchTask;
try {
await callback();
payload.operation_mode = newMode;
globalStore.putValue(meta.device, "opModeSwitchTask", null);
}
catch {
// do nothing when callback fails
}
}
else {
payload.operation_mode = (0, utils_1.getFromLookup)(dataObject247[155], { 0: "action_mode", 1: "scene_mode" });
}
}
const payload247 = await (0, exports.numericAttributes2Payload)(msg, meta, model, options, dataObject247);
payload = { ...payload, ...payload247 };
}
break;
case "258":
payload.detection_interval = value;
break;
case "268":
if (["RTCGQ13LM", "RTCGQ14LM", "RTCZCGQ11LM"].includes(model.model)) {
payload.motion_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "low", 2: "medium", 3: "high" });
}
else if (["JT-BZ-01AQ/A"].includes(model.model)) {
payload.gas_sensitivity = (0, utils_1.getFromLookup)(value, { 1: "15%LEL", 2: "10%LEL" });
}
break;
case "293":
payload.click_mode = (0, utils_1.getFromLookup)(value, { 1: "fast", 2: "multi" });
break;
case "294":
if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
payload.buzzer_manual_mute = value === 1;
}
break;
case "295":
if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
payload.test = value === 1;
}
break;
case "313":
if (["JT-BZ-01AQ/A"].includes(model.model)) {
payload.state = (0, utils_1.getFromLookup)(value, { 0: "work", 1: "preparation" });
}
break;
case "314":
if (["JT-BZ-01AQ/A"].includes(model.model)) {
payload.gas = value === 1;
}
else if (["JY-GZ-01AQ"].includes(model.model)) {
payload.smoke = value === 1;
}
break;
case "315":
if (["JT-BZ-01AQ/A"].includes(model.model)) {
payload.gas_density = value;
}
else if (["JY-GZ-01AQ"].includes(model.model)) {
payload.smoke_density = value;
payload.smoke_density_dbm = (0, utils_1.getFromLookup)(value, {
0: 0,
1: 0.085,
2: 0.088,
3: 0.093,
4: 0.095,
5: 0.1,
6: 0.105,
7: 0.11,
8: 0.115,
9: 0.12,
10: 0.125,
});
}
break;
case "316":
if (["JY-GZ-01AQ"].includes(model.model)) {
payload.heartbeat_indicator = value === 1;
}
break;
case "317":
if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
payload.buzzer_manual_alarm = value === 1;
}
break;
case "320":
if (["MCCGQ13LM"].includes(model.model)) {
payload.tamper = (0, utils_1.getFromLookup)(value, { 0: false, 1: true });
}
break;
case "322":
if (["RTCZCGQ11LM"].includes(model.model)) {
payload.presence = (0, utils_1.getFromLookup)(value, { 0: false, 1: true, 255: null });
}
break;
case "323":
if (["RTCZCGQ11LM"].includes(model.model)) {
payload.presence_event = (0, utils_1.getFromLookup)(value, {
0: "enter",
1: "leave",
2: "left_enter",
3: "right_leave",
4: "right_enter",
5: "left_leave",
6: "approach",
7: "away",
});
}
break;
case "324":
if (["RTCZCGQ11LM"].includes(model.model)) {
payload.monitoring_mode = (0, utils_1.getFromLookup)(value, { 0: "undirected", 1: "left_right" });
}
break;
case "326":
if (["RTCZCGQ11LM"].includes(model.model)) {
payload.approach_distance = (0, utils_1.getFromLookup)(value, { 0: "far", 1: "medium", 2: "near" });
}
break;
case "328":
if (["CTP-R01"].includes(model.model)) {
// detected hard switch of operation_mode (attribute 0x148[328])
payload.operation_mode = (0, utils_1.getFromLookup)(msg.data[328], { 0: "action_mode", 1: "scene_mode" });
}
break;
case "329":
if (["CTP-R01"].includes(model.model)) {
// side_up attribute report (attribute 0x149[329])
payload.action = "side_up";
payload.side = msg.data[329] + 1;
}
break;
case "331":
if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
payload.linkage_alarm = value === 1;
}
break;
case "332":
if (["JT-BZ-01AQ/A", "JY-GZ-01AQ"].includes(model.model)) {
payload.linkage_alarm_state = value === 1;
}
break;
case "338":
if (["RTCGQ14LM"].includes(model.model)) {
payload.trigger_indicator = value === 1;
}
break;
case "512":
if (["ZNCZ15LM", "QBCZ14LM", "QBCZ15LM", "SP-EUC01"].includes(model.model)) {
payload.button_lock = value === 1 ? "OFF" : "ON";
}
else {
const mode = (0, utils_1.getFromLookup)(value, { 1: "control_relay", 0: "decoupled" });
payload[(0, utils_1.postfixWithEndpointName)("operation_mode", msg, model, meta)] = mode;
}
break;
case "513":
payload.power_outage_memory = value === 1;
break;
case "514":
payload.auto_off = value === 1;
break;
case "515":
payload.led_disabled_night = value === 1;
break;
case "519":
payload.consumer_connected = value === 1;
break;
case "523":
(0, utils_1.assertNumber)(value);
payload.overload_protection = (0, utils_1.precisionRound)(value, 2);
break;
case "550":
payload.button_switch_mode = value === 1 ? "relay_and_usb" : "relay";
break;
case "645":
// aqara z1 lock relay
payload.lock_relay = value === 1;
break;
case "1025":
if (["ZNCLBL01LM"].includes(model.model)) {
payload.hand_open = !value;
}
else {
// next values update only when curtain finished initial setup and knows current position
// @ts-expect-error ignore
payload.options = { ...payload.options, reverse_direction: value[2] === "\u0001", hand_open: value[5] === "\u0000" };
}
break;
case "1028":
payload = {
...payload,
motor_state: (0, utils_1.getFromLookup)(value, options.invert_cover ? { 0: "stopped", 1: "closing", 2: "opening" } : { 0: "stopped", 1: "opening", 2: "closing" }),
running: !!value,
};
break;
case "1032":
if (["ZNJLBL01LM"].includes(model.model)) {
payload.motor_speed = (0, utils_1.getFromLookup)(value, { 0: "low", 1: "medium", 2: "high" });
}
break;
case "1033":
if (["ZNJLBL01LM"].includes(model.model)) {
payload.charging_status = value === 1;
}
break;
case "1034":
if (["ZNJLBL01LM"].includes(model.model)) {
payload.battery = value;
}
break;
case "1035":
if (["ZNCLBL01LM"].includes(model.model)) {
payload.voltage = value;
}
break;
case "1055":
if (["ZNCLBL01LM"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
payload.target_position = options.invert_cover ? 100 - value : value;
}
break;
case "1056":
if (["ZNCLBL01LM"].includes(model.model)) {
// This is the "target_state" attribute, which takes the following values: 0: 'OPEN', 1: 'CLOSE', 2: 'STOP'.
// It is not used because the values 0 and 1 are not always reported.
// https://github.com/Koenkk/zigbee-herdsman-converters/pull/4307
}
break;
case "1057":
if (["ZNCLBL01LM"].includes(model.model)) {
payload.motor_state = (0, utils_1.getFromLookup)(value, options.invert_cover ? { 0: "opening", 1: "closing", 2: "stopped" } : { 0: "closing", 1: "opening", 2: "stopped" });
(0, utils_1.assertNumber)(value);
payload.running = value < 2;
}
break;
case "1061":
if (["ZNCLBL01LM"].includes(model.model)) {
payload.action = (0, utils_1.getFromLookup)(value, options.invert_cover ? { 1: "manual_close", 2: "manual_open" } : { 1: "manual_open", 2: "manual_close" });
}
break;
case "1063":
if (["ZNCLBL01LM"].includes(model.model)) {
(0, utils_1.getFromLookup)(value, { 0: "UNLOCK", 1: "LOCK" });
}
break;
case "1064":
if (["ZNCLBL01LM"].includes(model.model)) {
payload.hooks_state = (0, utils_1.getFromLookup)(value, { 0: "unlocked", 1: "locked", 2: "locking", 3: "unlocking" });
payload.hooks_lock = (0, utils_1.getFromLookup)(value, { 0: "UNLOCK", 1: "LOCK", 2: "UNLOCK", 3: "LOCK" });
}
break;
case "1065":
if (["ZNCLBL01LM"].includes(model.model)) {
(0, utils_1.assertNumber)(value);
payload.illuminance = value * 50;
}
break;
case "1289":
payload.dimmer_mode = (0, utils_1.getFromLookup)(value, { 3: "rgbw", 1: "dual_ct" });
break;
case "1299":
if (["ZNXDD01LM"].includes(model.model)) {
// maximum color temp (6500)
}
break;
case "1300":
if (["ZNXDD01LM"].includes(model.model)) {
// minimum color temp (2700)
}
break;
case "65281":
{
// @ts-expect-error ignore
const payload65281 = await (0, exports.numericAttributes2Payload)(msg, meta, model, options, value);
payload = { ...payload, ...payload65281 };
}
break;
case "65282":
// This is a a complete structure with attributes, like element 0 for state, element 1 for voltage...
// At this moment we only extract what we are sure, for example, position 0 seems to be always 1 for a
// occupancy sensor, so we ignore it at this moment
// @ts-expect-error ignore
payload.voltage = value[1].elmVal;
if (model.meta?.battery?.voltageToPercentage) {
(0, utils_1.assertNumber)(payload.voltage);
payload.battery = (0, utils_1.batteryVoltageToPercentage)(payload.voltage, model.meta.battery.voltageToPercentage);
}
// @ts-expect-error ignore
payload.power_outage_count = value[4].elmVal - 1;
break;
case "mode":
(0, utils_1.assertNumber)(value);
payload.operation_mode = ["command", "event"][value];
break;
case "modelId":
// We ignore it, but we add it here to not shown an unknown key in the log
break;
case "illuminance":
// It contains the illuminance and occupancy, but in z2m we use a custom timer to do it, so we ignore it
break;
case "displayUnit":
// Use lumiDisplayUnit modernExtend, but we add it here to not shown an unknown key in the log
break;
case "airQuality":
// Use lumiAirQuality modernExtend, but we add it here to not shown an unknown key in the log
break;
default:
logger_1.logger.debug(`${model.model}: unknown key ${key} with value ${value}`, NS);
}
}
logger_1.logger.debug(`${model.model}: Processed data into payload ${JSON.stringify(payload)}`, NS);
return payload;
};
exports.numericAttributes2Payload = numericAttributes2Payload;
const numericAttributes2Lookup = async (model, dataObject) => {
let result = {};
for (const [key, value] of Object.entries(dataObject)) {
switch (key) {
case "247":
{
const dataObject247 = (0, exports.buffer2DataObject)(model, value);
const result247 = await numericAttributes2Lookup(model, dataObject247);
result = { ...result, ...result247 };
}
break;
case "65281":
{
const result65281 = await numericAttributes2Lookup(model, value);
result = { ...result, ...result65281 };
}
break;
default:
result[key] = value;
}
}
return result;
};
const lumiPresenceConstants = {
region_event_key: 0x0151,
region_event_types: {
Enter: 1,
Leave: 2,
Occupied: 4,
Unoccupied: 8,
},
region_config_write_attribute: 0x0150,
region_config_write_attribute_type: 0x41,
region_config_cmds: {
/**
* Creates new region (or force replaces existing one)
* with new zones definition.
*/
create: 1,
/**
* Modifies existing region.
* Note: unused, as it seems to break existing regions
* (region stops reporting new detection events).
* Use "create" instead, as it replaces existing region with new one.
*/
modify: 2,
/**
* Deletes existing region.
*/
delete: 3,
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
region_config_regionId_min: 1,
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
region_config_regionId_max: 10,
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
region_config_zoneY_min: 1,
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
region_config_zoneY_max: 7,
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
region_config_zoneX_min: 1,
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
region_config_zoneX_max: 4,
region_config_cmd_suffix_upsert: 0xff,
region_config_cmd_suffix_delete: 0x00,
};
const lumiPresenceMappers = {
lumi_presence: {
region_event_type_names: {
[lumiPresenceConstants.region_event_types.Enter]: "enter",
[lumiPresenceConstants.region_event_types.Leave]: "leave",
[lumiPresenceConstants.region_event_types.Occupied]: "occupied",
[lumiPresenceConstants.region_event_types.Unoccupied]: "unoccupied",
},
},
};
exports.presence = {
constants: lumiPresenceConstants,
mappers: lumiPresenceMappers,
encodeXCellsDefinition: (xCells) => {
if (!xCells?.length) {
return 0;
}
return [...xCells.values()].reduce((accumulator, marker) => accumulator + exports.presence.encodeXCellIdx(marker), 0);
},
encodeXCellIdx: (cellXIdx) => {
return 2 ** (cellXIdx - 1);
},
parseAqaraFp1RegionDeleteInput: (input) => {
if (!input || typeof input !== "object") {
return exports.presence.failure({ reason: "NOT_OBJECT" });
}
if (!("region_id" in input) || !exports.presence.isAqaraFp1RegionId(input.region_id)) {
return exports.presence.failure({ reason: "INVALID_REGION_ID" });
}
return {
isSuccess: true,
payload: {
command: {
region_id: input.region_id,
},
},
};
},
parseAqaraFp1RegionUpsertInput: (input) => {
if (!input || typeof input !== "object") {
return exports.presence.failure({ reason: "NOT_OBJECT" });
}
if (!("region_id" in input) || !exports.presence.isAqaraFp1RegionId(input.region_id)) {
return exports.presence.failure({ reason: "INVALID_REGION_ID" });
}
if (!("zones" in input) || !Array.isArray(input.zones) || !input.zones.length) {
return exports.presence.failure({ reason: "ZONES_LIST_EMPTY" });
}
if (!input.zones.every(exports.presence.isAqaraFp1RegionZoneDefinition)) {
return exports.presence.failure({ reason: "INVALID_ZONES" });
}
return {
isSuccess: true,
payload: {
command: {
region_id: input.region_id,
zones: input.zones,
},
},
};
},
// biome-ignore lint/suspicious/noExplicitAny: ignored using `--suppress`
isAqaraFp1RegionId: (value) => {
return (typeof value === "number" &&
value >= exports.presence.constants.region_config_regionId_min &&
value <= exports.presence.constants.region_config_regionId_max);
},
// biome-ignore lint/suspicious/noExplicitAny: ignored using `--suppress`
isAqaraFp1RegionZoneDefinition: (value) => {
return (value &&
typeof value === "object" &&
"x" in value &&
"y" in value &&
typeof value.x === "number" &&
typeof value.y === "number" &&
value.x >= exports.presence.constants.region_config_zoneX_min &&
value.x <= exports.presence.constants.region_config_zoneX_max &&
value.y >= exports.presence.constants.region_config_zoneY_min &&
value.y <= exports.presence.constants.region_config_zoneY_max);
},
failure: (error) => {
return {
isSuccess: false,
error,
};
},
};
function readTemperature(buffer, offset) {
return buffer.readUint16BE(offset) / 100;
}
function writeTemperature(buffer, offset, temperature) {
buffer.writeUInt16BE(temperature * 100, offset);
}
const dayNames = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
function readDaySelection(buffer, offset) {
const selectedDays = [];
dayNames.forEach((day, index) => {
if ((buffer[offset] >> (index + 1)) % 2 !== 0) {
selectedDays.push(day);
}
});
return selectedDays;
}
function validateDaySelection(selectedDays) {
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
selectedDays
.filter((selectedDay) => !dayNames.includes(selectedDay))
.forEach((invalidValue) => {
throw new Error(`The value "${invalidValue}" is not a valid day (available values: ${dayNames.join(", ")})`);
});
}
function writeDaySelection(buffer, offset, selectedDays) {
validateDaySelection(selectedDays);
const bitMap = dayNames.reduce((repeat, dayName, index) => {
const isDaySelected = selectedDays.includes(dayName);
// @ts-expect-error ignore
return repeat | (isDaySelected << (index + 1));
}, 0);
buffer.writeUInt8(bitMap, offset);
}
const timeNextDayFlag = 1 << 15;
function readTime(buffer, offset) {
const minutesWithDayFlag = buffer.readUint16BE(offset);
return minutesWithDayFlag & ~timeNextDayFlag;
}
function validateTime(time) {
const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
if (!isPositiveInteger(time)) {
throw new Error("Time must be a positive integer number");
}
if (time >= 24 * 60) {
throw new Error("Time must be between 00:00 and 23:59");
}
}
function writeTime(buffer, offset, time, isNextDay) {
validateTime(time);
let minutesWithDayFlag = time;
if (isNextDay) {
minutesWithDayFlag = minutesWithDayFlag | timeNextDayFlag;
}
buffer.writeUInt16BE(minutesWithDayFlag, offset);
}
/**
* Formats a number of minutes into a user-readable 24-hour time notation in the form hh:mm.
*/
function formatTime(timeMinutes) {
const hours = Math.floor(timeMinutes / 60);
const minutes = timeMinutes % 60;
return `${hours}:${String(minutes).padStart(2, "0")}`;
}
/**
* Parses a 24-hour time notation string in the form hh:mm into a number of minutes.
*/
function parseTime(timeString) {
const parts = timeString.split(":");
if (parts.length !== 2) {
throw new Error(`Cannot parse time string ${timeString}`);
}
const hours = Number.parseInt(parts[0]);
const minutes = Number.parseInt(parts[1]);
return hours * 60 + minutes;
}
const stringifiedScheduleFragmentSeparator = "|";
const stringifiedScheduleValueSeparator = ",";
exports.trv = {
decodeFirmwareVersionString(value) {
// Add prefix to follow Aqara's versioning schema: https://www.aqara.com/en/version/radiator-thermostat-e1
const firmwareVersionPrefix = "0.0.0_";
// Reinterpret from LE integer to byte sequence(e.g., `[25,8,0,0]` corresponds to 0.0.0_0825)
const buffer = node_buffer_1.Buffer.alloc(4);
buffer.writeUInt32LE(value);
const firmwareVersionNumber = (0, utils_1.toNumber)(buffer.reverse().subarray(1).join(""), "firmwareVersionNumber").toString().padStart(4, "0");
return firmwareVersionPrefix + firmwareVersionNumber;
},
decodePreset(value) {
// Setup mode is the initial device state after powering it ("F11" on display) and not a real preset that can be deliberately
// set by users, therefore it is exposed as a separate flag.
return {
setup: value === 3,
preset: { 2: "away", 1: "auto", 0: "manual" }[value],
};
},
decodeHeartbeat(meta, model, messageBuffer) {
const data = (0, exports.buffer2DataObject)(model, messageBuffer);
const payload = {};
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
Object.entries(data).forEach(([key, value]) => {
switch (Number.parseInt(key)) {
case 3:
payload.device_temperature = value;
break;
case 5:
(0, utils_1.assertNumber)(value);
payload.power_outage_count = value - 1;
break;
case 10:
// unidentified number, e.g. 32274, 3847
break;
case 13:
(0, utils_1.assertNumber)(value);
payload.firmware_version = exports.trv.decodeFirmwareVersionString(value);
break;
case 17:
// unidentified flag/enum, e.g. 1
break;
case 101:
(0, utils_1.assertNumber)(value);
Object.assign(payload, exports.trv.decodePreset(value));
break;
case 102:
(0, utils_1.assertNumber)(value);
payload.local_temperature = value / 100;
break;
case 103:
// This takes the following values:
// - `occupied_heating_setpoint` if `system_mode` is `heat` and `preset` is `manual`
// - `away_preset_temperature` if `system_mode` is `heat` and `preset` is `away`
// - `5` if `system_mode` is `off`
// It thus behaves similar to `occupied_heating_setpoint` except in `off` mode. Due to this difference,
// this value is written to another property to avoid an inconsistency of the `occupied_heating_setpoint`.
// TODO How to handle this value? Find better name?
(0, utils_1.assertNumber)(value);
payload.internal_heating_setpoint = value / 100;
break;
case 104:
payload.valve_alarm = value === 1;
break;
case 105:
payload.battery = value;
break;
case 106:
// unidentified flag/enum, e.g. 0
break;
}
});
return payload;
},
/**
* Decode a Zigbee schedule configuration message into a schedule configuration object.
*/
decodeSchedule(buffer) {
return {
days: readDaySelection(buffer, 1),
events: [
{ time: readTime(buffer, 2), temperature: readTemperature(buffer, 6) },
{ time: readTime(buffer, 8), temperature: readTemperature(buffer, 12) },
{ time: readTime(buffer, 14), temperature: readTemperature(buffer, 18) },
{ time: readTime(buffer, 20), temperature: readTemperature(buffer, 24) },
],
};
},
validateSchedule(schedule) {
const eventCount = 4;
validateDaySelection(schedule.days);
if (schedule.events.length !== eventCount) {
throw new Error(`The schedule object must contain an array of ${eventCount} time/temperature events`);
}
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
schedule.events.forEach((event) => {
validateTime(event.time);
if (event.temperature < 5 || event.temperature > 30) {
throw new Error("The temperature must be between 5 and 30 °C");
}
});
// Calculate time durations between events
const durations = schedule.events
.map((entry, index, entries) => {
if (index === 0) {
return 0;
}
const time = entry.time;
const fullDay = 24 * 60;
const previousTime = entries[index - 1].time;
const isNextDay = time < previousTime;
if (isNextDay) {
return fullDay - previousTime + time;
}
return time - previousTime;
})
// Remove first entry which is not a duration
.slice(1);
const minDuration = 60;
const hasInvalidDurations = durations.some((duration) => duration < minDuration);
if (hasInvalidDurations) {
throw new Error("The individual times must be at least 1 hour apart");
}
const maxTotalDuration = 24 * 60;
const totalDuration = durations.reduce((total, duration) => total + duration, 0);
if (totalDuration > maxTotalDuration) {
// this implicitly also makes sure that there is at most one "next day" switch
throw new Error("The start and end times must be at most 24 hours apart");
}
},
/**
* Encodes a schedule object into Zigbee message format.
*/
encodeSchedule(schedule) {
const buffer = node_buffer_1.Buffer.alloc(26);
buffer.writeUInt8(0x04);
writeDaySelection(buffer, 1, schedule.days);
schedule.events.forEach((event, index, events) => {
const offset = 2 + index * 6;
const isNextDay = index > 0 && event.time < events[index - 1].time;
writeTime(buffer, offset, event.time, isNextDay);
writeTemperature(buffer, offset + 4, event.temperature);
});
return buffer;
},
stringifySchedule(schedule) {
const stringifiedScheduleFragments = [schedule.days.join(stringifiedScheduleValueSeparator)];
for (const event of schedule.events) {
const formattedTemperature = Number.isInteger(event.temperature)
? event.temperature.toFixed(1) // add ".0" for usability to signal that floats can be used
: String(event.temperature);
const entryFragments = [formatTime(event.time), formattedTemperature];
stringifiedScheduleFragments.push(entryFragments.join(stringifiedScheduleValueSeparator));
}
return stringifiedScheduleFragments.join(stringifiedScheduleFragmentSeparator);
},
// Parses a schedule configuration string into a configuration object.
parseSchedule(stringifiedSchedule) {
const schedule = { days: [], events: [] };
if (!stringifiedSchedule) {
return schedule;
}
const stringifiedScheduleFragments = stringifiedSchedule.split(stringifiedScheduleFragmentSeparator);
stringifiedScheduleFragments.forEach((fragment, index) => {
if (index === 0) {
// @ts-expect-error ignore
schedule.days.push(...fragment.split(stringifiedScheduleValueSeparator));
}
else {
const entryFragments = fragment.split(stringifiedScheduleValueSeparator);
const entry = { time: parseTime(entryFragments[0]), temperature: Number.parseFloat(entryFragments[1]) };
schedule.events.push(entry);
}
});
return schedule;
},
};
exports.manufacturerCode = 0x115f; // TODO: from Zcl
const manufacturerOptions = {
lumi: { manufacturerCode: exports.manufacturerCode, disableDefaultResponse: true },
};
exports.lumiModernExtend = {
lumiLight: (args) => {
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
args = { powerOutageCount: true, deviceTemperature: true, ...args };
const colorTemp = args.colorTemp ? { startup: false, range: args.colorTempRange ?? [153, 370] } : undefined;
const result = modernExtend.light({ effect: false, powerOnBehavior: false, ...args, colorTemp });
result.fromZigbee.push(exports.fromZigbee.lumi_bulb_interval, fz.ignore_occupancy_report, fz.ignore_humidity_report, fz.ignore_pressure_report, fz.ignore_temperature_report, exports.fromZigbee.lumi_specific);
if (args.powerOutageCount)
result.exposes.push(e.power_outage_count());
if (args.deviceTemperature)
result.exposes.push(e.device_temperature());
if (args.powerOutageMemory === "switch") {
result.toZigbee.push(exports.toZigbee.lumi_switch_power_outage_memory);
result.exposes.push(e.power_outage_memory());
}
else if (args.powerOutageMemory === "light") {
result.toZigbee.push(exports.toZigbee.lumi_light_power_outage_memory);
result.exposes.push(e.power_outage_memory().withAccess(ea.STATE_SET));
}
else if (args.powerOutageMemory === "enum") {
const extend = exports.lumiModernExtend.lumiPowerOnBehavior({ lookup: { on: 0, previous: 1, off: 2 } });
result.toZigbee.push(...extend.toZigbee);
result.exposes.push(...extend.exposes);
}
return result;
},
lumiOnOff: (args) => {
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
args = { operationMode: false, lockRelay: false, ...args };
const result = modernExtend.onOff({ powerOnBehavior: false, ...args });
result.fromZigbee.push(exports.fromZigbee.lumi_specific);
result.exposes.push(e.device_temperature(), e.power_outage_count());
if (args.powerOutageMemory === "binary") {
const extend = exports.lumiModernExtend.lumiPowerOutageMemory();
result.toZigbee.push(...extend.toZigbee);
result.exposes.push(...extend.exposes);
}
else if (args.powerOutageMemory === "enum") {
const extend = exports.lumiModernExtend.lumiPowerOnBehavior();
result.toZigbee.push(...extend.toZigbee);
result.exposes.push(...extend.exposes);
}
if (args.operationMode === true) {
const extend = exports.lumiModernExtend.lumiOperationMode({ description: "Decoupled mode for a button" });
if (args.endpointNames) {
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
args.endpointNames.forEach((ep) => {
const epExtend = exports.lumiModernExtend.lumiOperationMode({
description: `Decoupled mode for ${ep.toString()} button`,
endpointName: ep,
});
result.toZigbee.push(...epExtend.toZigbee);
result.exposes.push(...epExtend.exposes);
});
}
else {
result.toZigbee.push(...extend.toZigbee);
result.exposes.push(...extend.exposes);
}
}
if (args.lockRelay) {
const extend = exports.lumiModernExtend.lumiLockRelay();
if (args.endpointNames) {
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
args.endpointNames.forEach((ep) => {
const epExtend = exports.lumiModernExtend.lumiLockRelay({
description: `Locks ${ep.toString()} relay and prevents it from operating`,
endpointName: ep,
});
result.toZigbee.push(...epExtend.toZigbee);
result.exposes.push(...epExtend.exposes);
});
}
else {
result.toZigbee.push(...extend.toZigbee);
result.exposes.push(...extend.exposes);
}
}
return result;
},
lumiSwitchType: (args) => modernExtend.enumLookup({
name: "switch_type",
lookup: { toggle: 1, momentary: 2, none: 3 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x000a, type: 0x20 },
description: "External switch type",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiMotorSpeed: (args) => modernExtend.enumLookup({
name: "motor_speed",
lookup: { low: 0, medium: 1, high: 2 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x0408, type: 0x20 },
description: "Controls the motor speed",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiCurtainSpeed: (args) => modernExtend.numeric({
name: "curtain_speed",
cluster: "manuSpecificLumi",
attribute: { ID: 0x043b, type: 0x20 },
description: "Speed of curtain movement",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "ALL",
unit: "%",
valueMin: 1,
valueMax: 100,
entityCategory: "config",
...args,
}),
lumiCurtainManualOpenClose: (args) => modernExtend.binary({
name: "manual_open_close",
valueOn: ["ON", 1],
valueOff: ["OFF", 0],
cluster: "manuSpecificLumi",
attribute: "curtainHandOpen",
description: "Gently pull to open/close the curtain automatically",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "ALL",
entityCategory: "config",
...args,
}),
lumiCurtainAdaptivePullingSpeed: (args) => modernExtend.binary({
name: "adaptive_pulling_speed",
valueOn: ["ON", 1],
valueOff: ["OFF", 0],
cluster: "manuSpecificLumi",
attribute: { ID: 0x0442, type: 0x20 },
description: "The faster/slower the curtain is pulled manually, the faster/slower the curtain will move",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "ALL",
entityCategory: "config",
...args,
}),
lumiCurtainManualStop: (args) => modernExtend.binary({
name: "manual_stop",
valueOn: ["ON", 1],
valueOff: ["OFF", 0],
cluster: "manuSpecificLumi",
attribute: { ID: 0x043a, type: 0x10 },
description: "Manually pulling the curtain during operation stops the motor",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "ALL",
entityCategory: "config",
...args,
}),
lumiCurtainReverse: (args) => modernExtend.binary({
name: "reverse_direction",
valueOn: [true, 1],
valueOff: [false, 0],
cluster: "closuresWindowCovering",
attribute: "windowCoveringMode",
description: "Whether the curtain direction is inverted",
access: "ALL",
entityCategory: "config",
...args,
}),
lumiCurtainStatus: (args) => modernExtend.enumLookup({
name: "status",
lookup: { closing: 0, opening: 1, stopped: 2, blocked: 3 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x0421, type: 0x20 },
description: "Current status of the curtain (Opening, Closing, Stopped, Blocked)",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "STATE",
entityCategory: "diagnostic",
...args,
}),
lumiCurtainLastManualOperation: (args) => modernExtend.enumLookup({
name: "last_manual_operation",
lookup: { open: 1, close: 2, stop: 3 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x0425, type: 0x20 },
description: "Last triggered manual operation",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "STATE",
entityCategory: "diagnostic",
...args,
}),
lumiCurtainPosition: (args) => modernExtend.numeric({
name: "curtain_position",
cluster: "manuSpecificLumi",
attribute: { ID: 0x041f, type: 0x20 },
description: "Current position of the curtain",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "STATE",
unit: "%",
valueMin: 1,
valueMax: 100,
entityCategory: "diagnostic",
...args,
}),
lumiCurtainTraverseTime: (args) => modernExtend.numeric({
name: "traverse_time",
cluster: "manuSpecificLumi",
attribute: { ID: 0x0403, type: 0x20 },
description: "Time in seconds to get from one end to another",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "STATE",
unit: "sec",
entityCategory: "diagnostic",
...args,
}),
lumiCurtainCalibrationStatus: (args) => modernExtend.enumLookup({
name: "calibration_status",
lookup: { not_calibrated: 0, half_calibrated: 1, fully_calibrated: 2 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x0426, type: 0x20 },
description: "Calibration status of the curtain (Not calibrated, Half calibrated, Fully calibrated)",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "STATE",
entityCategory: "diagnostic",
...args,
}),
lumiCurtainCalibrated: (args) => modernExtend.binary({
name: "calibrated",
valueOn: [true, 1],
valueOff: [false, 0],
cluster: "manuSpecificLumi",
attribute: "curtainCalibrated",
description: "Indicates if this device is calibrated",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "STATE",
entityCategory: "diagnostic",
...args,
}),
lumiCurtainIdentifyBeep: (args) => modernExtend.enumLookup({
name: "identify_beep",
lookup: { short: 0, "1_sec": 1, "2_sec": 2 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x0404, type: 0x20 },
description: "Device will beep for chosen time duration",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "ALL",
entityCategory: "config",
...args,
}),
lumiPowerOnBehavior: (args) => modernExtend.enumLookup({
name: "power_on_behavior",
lookup: { on: 0, previous: 1, off: 2, inverted: 3 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x0517, type: 0x20 },
description: "Controls the behavior when the device is powered on after power loss",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiPowerOutageMemory: (args) => modernExtend.binary({
name: "power_outage_memory",
cluster: "manuSpecificLumi",
attribute: { ID: 0x0201, type: 0x10 },
valueOn: [true, 1],
valueOff: [false, 0],
description: "Controls the behavior when the device is powered on after power loss",
access: "ALL",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiOperationMode: (args) => modernExtend.enumLookup({
name: "operation_mode",
lookup: { decoupled: 0, control_relay: 1 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x0200, type: 0x20 },
description: "Decoupled mode for relay",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiAction: (args) => modernExtend.actionEnumLookup({
actionLookup: { single: 1 },
cluster: "genMultistateInput",
attribute: "presentValue",
...args,
}),
lumiVoc: (args) => modernExtend.numeric({
name: "voc",
cluster: "genAnalogInput",
attribute: "presentValue",
reporting: { min: "10_SECONDS", max: "1_HOUR", change: 5 },
description: "Measured VOC value",
unit: "ppb",
access: "STATE_GET",
...args,
}),
lumiAirQuality: (args) => modernExtend.enumLookup({
name: "air_quality",
lookup: { excellent: 1, good: 2, moderate: 3, poor: 4, unhealthy: 5, unknown: 0 },
cluster: "manuSpecificLumi",
attribute: "airQuality",
zigbeeCommandOptions: { disableDefaultResponse: true },
description: "Measured air quality",
access: "STATE_GET",
...args,
}),
lumiDisplayUnit: (args) => modernExtend.enumLookup({
name: "display_unit",
lookup: {
mgm3_celsius: 0x00, // mg/m³, °C (default)
ppb_celsius: 0x01, // ppb, °C
mgm3_fahrenheit: 0x10, // mg/m³, °F
ppb_fahrenheit: 0x11, // ppb, °F
},
cluster: "manuSpecificLumi",
attribute: "displayUnit",
zigbeeCommandOptions: { disableDefaultResponse: true },
description: "Units to show on the display",
entityCategory: "config",
...args,
}),
lumiOutageCountRestoreBindReporting: () => {
const fromZigbee = [
{
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: async (model, msg, publish, options, meta) => {
// At least the Aqara TVOC sensor does not send a deviceAnnounce after coming back online.
// The reconfigureReportingsOnDeviceAnnounce modernExtend is not usable because of this,
// there is however an outage counter published in the 'special' buffer data reported
// under the manuSpecificLumi cluster as attribute 247, we simple decode and grab value with ID 5.
// Normal attribute publishing and decoding will be left to the classic fromZigbee or modernExtends.
if (msg.data["247"] !== undefined) {
const dataDecoded = (0, exports.buffer2DataObject)(model, msg.data["247"]);
if (dataDecoded["5"] !== undefined) {
(0, utils_1.assertNumber)(dataDecoded["5"]);
const currentOutageCount = dataDecoded["5"] - 1;
const previousOutageCount = meta.device?.meta?.outageCount ? meta.device.meta.outageCount : 0;
if (currentOutageCount > previousOutageCount) {
logger_1.logger.debug(`Restoring binding and reporting, ${msg.device.ieeeAddr} came back after losing power.`, NS);
// update outageCount in database
meta.device.meta.outageCount = currentOutageCount;
meta.device.save();
// restore binding
for (const endpoint of meta.device.endpoints) {
// restore bindings
for (const b of endpoint.binds) {
try {
await endpoint.bind(b.cluster.name, b.target);
}
catch (error) {
logger_1.logger.debug(`Failed to re-bind ${b.cluster.name} from ${b.target} for ${msg.device.ieeeAddr} (${error})`, NS);
}
}
// restore reporting
for (const c of endpoint.configuredReportings) {
try {
await endpoint.configureReporting(c.cluster.name, [
{
attribute: c.attribute.name,
minimumReportInterval: c.minimumReportInterval,
maximumReportInterval: c.maximumReportInterval,
reportableChange: c.reportableChange,
},
]);
}
catch {
logger_1.logger.debug(`Failed to re-setup reporting of ${c.cluster.name}/${c.attribute.name} for ${msg.device.ieeeAddr}.`, NS);
}
}
}
}
}
}
},
},
];
return { fromZigbee, isModernExtend: true };
},
lumiZigbeeOTA: () => {
// Many Lumi devices miss OTA on endpoint 1 even while supporting it.
// https://github.com/Koenkk/zigbee2mqtt/issues/10660
const result = modernExtend.quirkAddEndpointCluster({
endpointID: 1,
outputClusters: ["genOta"],
});
result.ota = true;
return result;
},
lumiPower: (args) => modernExtend.numeric({
name: "power",
cluster: "genAnalogInput",
attribute: "presentValue",
reporting: { min: "10_SECONDS", max: "1_HOUR", change: 5 },
description: "Instantaneous measured power",
unit: "W",
access: "STATE",
entityCategory: "diagnostic",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiElectricityMeter: () => {
const exposes = [e.energy(), e.voltage(), e.current()];
const fromZigbee = [
{
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: async (model, msg, publish, options, meta) => {
return await (0, exports.numericAttributes2Payload)(msg, meta, model, options, msg.data);
},
},
];
return { exposes, fromZigbee, isModernExtend: true };
},
lumiOverloadProtection: (args) => modernExtend.numeric({
name: "overload_protection",
cluster: "manuSpecificLumi",
attribute: { ID: 0x020b, type: 0x39 },
description: "Maximum allowed load, turns off if exceeded",
valueMin: 100,
valueMax: 3840,
unit: "W",
access: "ALL",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiLedIndicator: (args) => modernExtend.binary({
name: "led_indicator",
cluster: "manuSpecificLumi",
attribute: { ID: 0x0203, type: 0x10 },
valueOn: ["ON", 1],
valueOff: ["OFF", 0],
description: "LED indicator",
access: "ALL",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiLedDisabledNight: (args) => modernExtend.binary({
name: "led_disabled_night",
cluster: "manuSpecificLumi",
attribute: { ID: 0x0203, type: 0x10 },
valueOn: [true, 1],
valueOff: [false, 0],
description: "Enables/disables LED indicator at night",
access: "ALL",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
reporting: false,
...args,
}),
lumiButtonLock: (args) => modernExtend.binary({
name: "button_lock",
cluster: "manuSpecificLumi",
attribute: { ID: 0x0200, type: 0x20 },
valueOn: ["ON", 0],
valueOff: ["OFF", 1],
description: "Disables the physical switch button",
access: "ALL",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiFlipIndicatorLight: (args) => modernExtend.binary({
name: "flip_indicator_light",
cluster: "manuSpecificLumi",
attribute: { ID: 0x00f0, type: 0x20 },
valueOn: ["ON", 1],
valueOff: ["OFF", 0],
description: "After turn on, the indicator light turns on while switch is off, and vice versa",
access: "ALL",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
reporting: false,
...args,
}),
lumiPreventReset: () => {
const onEvent = [
async (type, data, device) => {
if (
// options.allow_reset ||
type !== "message" ||
data.type !== "attributeReport" ||
data.cluster !== "genBasic" ||
!data.data[0xfff0] ||
// eg: [0xaa, 0x10, 0x05, 0x41, 0x87, 0x01, 0x01, 0x10, 0x00]
!data.data[0xfff0].slice(0, 5).equals(node_buffer_1.Buffer.from([0xaa, 0x10, 0x05, 0x41, 0x87]))) {
return;
}
const payload = {
[0xfff0]: {
value: [0xaa, 0x10, 0x05, 0x41, 0x47, 0x01, 0x01, 0x10, 0x01],
type: 0x41,
},
};
await device.getEndpoint(1).write("genBasic", payload, { manufacturerCode: exports.manufacturerCode });
},
];
return { onEvent, isModernExtend: true };
},
lumiClickMode: (args) => modernExtend.enumLookup({
name: "click_mode",
lookup: { fast: 1, multi: 2 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x0125, type: 0x20 },
description: "Click mode for wireless button. fast: only supports single click but allows faster response time." +
"multi: supports multiple types of clicks but is slower, because it awaits multiple clicks.",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiSlider: () => {
const fromZigbee = [
{
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data[652] !== undefined) {
const actionLookup = {
1: "slider_single",
2: "slider_double",
3: "slider_hold",
4: "slider_up",
5: "slider_down",
};
return {
action_slide_time: msg.data[561],
action_slide_speed: msg.data[562],
action_slide_relative_displacement: msg.data[563],
action: actionLookup[msg.data[652]],
action_slide_time_delta: msg.data[769],
};
}
},
},
];
const exposes = [
e.numeric("action_slide_time", ea.STATE).withUnit("ms").withCategory("diagnostic"),
e.numeric("action_slide_speed", ea.STATE).withUnit("mm/s").withCategory("diagnostic"),
e.numeric("action_slide_relative_displacement", ea.STATE).withCategory("diagnostic"),
e
.numeric("action_slide_time_delta", ea.STATE)
.withUnit("ms")
.withCategory("diagnostic"),
// action is exposed from extraActions inside lumiAction
];
return { fromZigbee, exposes, isModernExtend: true };
},
lumiLockRelay: (args) => modernExtend.binary({
name: "lock_relay",
cluster: "manuSpecificLumi",
attribute: { ID: 0x0285, type: 0x20 },
valueOn: [true, 1],
valueOff: [false, 0],
description: "Locks relay and prevents it from operating",
access: "ALL",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiSetEventMode: () => {
// I have no idea, why it is used everywhere, even if not supported
// modes:
// 0 - 'command' mode. keys send commands. useful for binding
// 1 - 'event' mode. keys send events. useful for handling
const configure = [
async (device, coordinatorEndpoint, definition) => {
await device.getEndpoint(1).write("manuSpecificLumi", { mode: 1 }, { manufacturerCode: exports.manufacturerCode, disableResponse: true });
},
];
return { configure, isModernExtend: true };
},
lumiSwitchMode: (args) => modernExtend.enumLookup({
name: "mode_switch",
lookup: { quick_mode: 1, anti_flicker_mode: 4 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x0004, type: 0x21 },
description: "Anti flicker mode can be used to solve blinking issues of some lights. Quick mode makes the device respond faster.",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiVibration: () => {
const exposes = [e.action(["shake", "triple_strike", "movement"])];
const fromZigbee = [
{
cluster: "ssIasZone",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data[45] !== undefined) {
const zoneStatus = msg.data[45];
const actionLookup = { 1: "shake", 2: "triple_strike" };
return { action: actionLookup[zoneStatus] };
}
},
},
{
cluster: "genMultistateInput",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.presentValue !== undefined && msg.data.presentValue === 1) {
return { action: "triple_strike" };
}
},
},
{
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data[0x0118] !== undefined && msg.data[0x0118] === 1) {
return { action: "movement" };
}
},
},
];
return { exposes, fromZigbee, isModernExtend: true };
},
lumiSensitivityAdjustment: (args) => modernExtend.enumLookup({
name: "sensitivity_adjustment",
lookup: { high: 1, medium: 2, low: 3 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x010e, type: 0x20 },
description: "Sensitivity adjustment for the device",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "SET",
entityCategory: "config",
...args,
}),
lumiReportInterval: (args) => modernExtend.enumLookup({
name: "report_interval",
lookup: { "1s": 0x01, "5s": 0x02, "10s": 0x03 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x0110, type: 0x20 },
description: "Reporting interval for the device",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
access: "SET",
entityCategory: "config",
...args,
}),
lumiMiscellaneous: (args) => {
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
args = { cluster: "manuSpecificLumi", deviceTemperatureAttribute: 3, powerOutageCountAttribute: 5, resetsWhenPairing: false, ...args };
const exposes = [e.device_temperature(), e.power_outage_count(args.resetsWhenPairing)];
const fromZigbee = [
{
cluster: args.cluster,
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const payload = {};
if (msg.data[args.deviceTemperatureAttribute] !== undefined) {
const value = msg.data[args.deviceTemperatureAttribute];
(0, utils_1.assertNumber)(value);
payload.device_temperature = value;
}
if (msg.data[args.powerOutageCountAttribute] !== undefined) {
const value = msg.data[args.powerOutageCountAttribute];
(0, utils_1.assertNumber)(value);
payload.power_outage_count = value - 1;
}
return payload;
},
},
];
return { exposes, fromZigbee, isModernExtend: true };
},
lumiKnobRotation: (args) => {
const withButtonState = args?.withButtonState || true;
const exposes = [
e.action(["start_rotating", "rotation", "stop_rotating"]),
e.numeric("action_rotation_angle", ea.STATE).withUnit("*").withDescription("Rotation angle").withCategory("diagnostic"),
e.numeric("action_rotation_angle_speed", ea.STATE).withUnit("*").withDescription("Rotation angle speed").withCategory("diagnostic"),
e.numeric("action_rotation_percent", ea.STATE).withUnit("%").withDescription("Rotation percent").withCategory("diagnostic"),
e.numeric("action_rotation_percent_speed", ea.STATE).withUnit("%").withDescription("Rotation percent speed").withCategory("diagnostic"),
e.numeric("action_rotation_time", ea.STATE).withUnit("ms").withDescription("Rotation time").withCategory("diagnostic"),
];
if (withButtonState) {
exposes.push(e
.enum("action_rotation_button_state", ea.STATE, ["released", "pressed"])
.withDescription("Button state during rotation")
.withCategory("diagnostic"));
}
const fromZigbee = [
{
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data[570] !== undefined) {
const act = { 1: "start_rotating", 2: "rotation", 3: "stop_rotating" };
const state = { 0: "released", 128: "pressed" };
return {
action: act[msg.data[570] & ~128],
action_rotation_button_state: state[msg.data[570] & 128],
action_rotation_angle: msg.data[558],
action_rotation_angle_speed: msg.data[560],
action_rotation_percent: msg.data[563],
action_rotation_percent_speed: msg.data[562],
action_rotation_time: msg.data[561],
};
}
},
},
];
return { exposes, fromZigbee, isModernExtend: true };
},
lumiCommandMode: (args) => {
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
args = { setEventMode: true, ...args };
const exposes = [
e
.enum("operation_mode", ea.ALL, ["event", "command"])
.withDescription("Command mode is useful for binding. Event mode is useful for processing."),
];
const toZigbee = [
{
key: ["operation_mode"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertString)(value);
// modes:
// 0 - 'command' mode. keys send commands. useful for binding
// 1 - 'event' mode. keys send events. useful for handling
const lookup = { command: 0, event: 1 };
const endpoint = meta.device.getEndpoint(1);
await endpoint.write("manuSpecificLumi", { mode: (0, utils_1.getFromLookup)(value.toLowerCase(), lookup) }, { manufacturerCode: manufacturerOptions.lumi.manufacturerCode });
return { state: { operation_mode: value.toLowerCase() } };
},
convertGet: async (entity, key, meta) => {
const endpoint = meta.device.getEndpoint(1);
await endpoint.read("manuSpecificLumi", ["mode"], { manufacturerCode: manufacturerOptions.lumi.manufacturerCode });
},
},
];
const result = { exposes, toZigbee, isModernExtend: true };
if (args.setEventMode) {
result.configure = exports.lumiModernExtend.lumiSetEventMode().configure;
}
return result;
},
lumiBattery: (args) => {
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
args = {
cluster: "manuSpecificLumi",
percentageAtrribute: 1,
voltageAttribute: 1,
...args,
};
const exposes = [e.battery(), e.battery_voltage()];
const fromZigbee = [
{
cluster: args.cluster,
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const payload = {};
const lookup = numericAttributes2Lookup(model, msg.data);
if (lookup[args.percentageAtrribute.toString()]) {
const value = lookup[args.percentageAtrribute];
(0, utils_1.assertNumber)(value);
if (!args.voltageToPercentage)
payload.battery = value;
}
if (lookup[args.voltageAttribute.toString()]) {
const value = lookup[args.voltageAttribute];
(0, utils_1.assertNumber)(value);
payload.voltage = value;
if (args.voltageToPercentage)
payload.battery = (0, utils_1.batteryVoltageToPercentage)(value, args.voltageToPercentage);
}
return payload;
},
},
];
return { exposes, fromZigbee, isModernExtend: true };
},
fp1ePresence: () => {
const attribute = { ID: 0x0142, type: 0x20 };
return modernExtend.binary({
name: "presence",
valueOn: [true, 1],
valueOff: [false, 0],
access: "STATE_GET",
cluster: "manuSpecificLumi",
attribute: attribute,
description: "Indicates whether the device detected presence",
});
},
fp1eMovement: () => modernExtend.enumLookup({
name: "movement",
lookup: { unknown_0: 0, unknown_1: 1, no_presence: 2, movement: 3, no_movement: 4 },
cluster: "manuSpecificLumi",
attribute: { ID: 0x0160, type: 0x20 },
zigbeeCommandOptions: { disableDefaultResponse: true },
description: "Is movement detected?",
access: "STATE_GET",
}),
fp1eTargetDistance: () => modernExtend.numeric({
name: "target_distance",
cluster: "manuSpecificLumi",
attribute: { ID: 0x015f, type: 0x23 },
access: "STATE_GET",
description: "Distance to the detected target",
scale: 100,
precision: 2,
unit: "m",
}),
fp1eDetectionRange: () => modernExtend.numeric({
name: "detection_range",
cluster: "manuSpecificLumi",
attribute: { ID: 0x015b, type: 0x23 },
access: "ALL",
description: "The device will monitor presence within the detection range",
scale: 100,
precision: 2,
unit: "m",
valueMin: 0,
valueMax: 6.0,
valueStep: 0.3,
}),
fp1eSpatialLearning: () => {
return {
isModernExtend: true,
exposes: [e.enum("spatial_learning", ea.SET, ["Start Learning"]).withDescription("Initiate AI Spatial Learning.")],
toZigbee: [
{
key: ["spatial_learning"],
convertSet: async (entity, key, value, meta) => {
await entity.write("manuSpecificLumi", { 343: { value: 1, type: 0x20 } }, manufacturerOptions.lumi);
},
},
],
};
},
fp1eRestartDevice: () => {
return {
isModernExtend: true,
exposes: [e.enum("restart_device", ea.SET, ["Restart Device"]).withDescription("Restarts the device.")],
toZigbee: [
{
key: ["restart_device"],
convertSet: async (entity, key, value, meta) => {
await entity.write("manuSpecificLumi", { 232: { value: 0x00, type: 0x10 } }, manufacturerOptions.lumi);
},
},
],
};
},
lumiMultiClick: (args) => modernExtend.binary({
name: "multi_click",
cluster: "manuSpecificLumi",
attribute: { ID: 0x0286, type: 0x20 },
valueOn: [true, 2],
valueOff: [false, 1],
description: "Enable multi-click mode for the switch, otherwise single click",
access: "ALL",
entityCategory: "config",
zigbeeCommandOptions: { manufacturerCode: exports.manufacturerCode },
...args,
}),
lumiPreventLeave: () => {
const onEvent = [
async (type, data, device) => {
if (type === "message" && data.type === "attributeReport" && data.cluster === "manuSpecificLumi" && data.data[0x00fc] === false) {
const payload = {
[0x00fc]: {
value: true,
type: 0x10,
},
};
await device.getEndpoint(1).write("manuSpecificLumi", payload, { manufacturerCode: exports.manufacturerCode });
}
},
];
return { onEvent, isModernExtend: true };
},
lumiExternalSensor: () => {
return {
isModernExtend: true,
exposes: [
e
.enum("sensor", ea.ALL, ["internal", "external"])
.withDescription("Select mode to display sensor: internal or both with external")
.withCategory("config"),
e
.numeric("external_temperature", ea.STATE_SET)
.withUnit("°C")
.withValueMin(-100)
.withValueMax(100)
.withValueStep(0.1)
.withDescription("Value for external temperature sensor")
.withCategory("config"),
e
.numeric("external_humidity", ea.STATE_SET)
.withUnit("%")
.withValueMin(0)
.withValueMax(100)
.withDescription("Value for external humidity sensor")
.withCategory("config"),
],
toZigbee: [
{
key: ["sensor", "external_temperature", "external_humidity"],
convertSet: async (entity, key, value, meta) => {
const lumiHeader = (counter, length, action) => {
const header = [0xaa, 0x71, length + 3, 0x44, counter];
const integrity = 512 - header.reduce((sum, elem) => sum + elem, 0);
return [...header, integrity, action, 0x41, length];
};
const fictiveSensor = node_buffer_1.Buffer.from("00158d00019d1b98", "hex");
switch (key) {
case "sensor": {
(0, utils_1.assertEndpoint)(entity);
const device = node_buffer_1.Buffer.from(entity.deviceIeeeAddress.substring(2), "hex");
const timestamp = node_buffer_1.Buffer.alloc(4);
timestamp.writeUInt32BE(Date.now() / 1000);
if (value === "external") {
const params1 = [
...timestamp,
0x15,
...device,
...fictiveSensor,
0x00,
0x02,
0x00,
0x55,
0x15,
0x0a,
0x01,
0x00,
0x00,
0x01,
0x06,
0xe6,
0xb9,
0xbf,
0xe5,
0xba,
0xa6,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x02,
0x08,
0x65,
];
const params2 = [
...timestamp,
0x14,
...device,
...fictiveSensor,
0x00,
0x01,
0x00,
0x55,
0x15,
0x0a,
0x01,
0x00,
0x00,
0x01,
0x06,
0xe6,
0xb8,
0xa9,
0xe5,
0xba,
0xa6,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x02,
0x07,
0x63,
];
const val1 = [...lumiHeader(0x12, params1.length, 0x02), ...params1];
const val2 = [...lumiHeader(0x13, params2.length, 0x02), ...params2];
await entity.write("manuSpecificLumi", { 65522: { value: val1, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
await entity.write("manuSpecificLumi", { 65522: { value: val2, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
}
else if (value === "internal") {
const params1 = [
...timestamp,
0x15,
...device,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
];
const params2 = [
...timestamp,
0x14,
...device,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
];
const val1 = [...lumiHeader(0x12, params1.length, 0x04), ...params1];
const val2 = [...lumiHeader(0x13, params2.length, 0x04), ...params2];
await entity.write("manuSpecificLumi", { 65522: { value: val1, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
await entity.write("manuSpecificLumi", { 65522: { value: val2, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
}
await entity.read("manuSpecificLumi", [0x172], { manufacturerCode: exports.manufacturerCode });
break;
}
case "external_temperature":
if (meta.state.sensor === "external") {
const temperatureBuf = node_buffer_1.Buffer.alloc(4);
const number = (0, utils_1.toNumber)(value);
temperatureBuf.writeFloatBE(Math.round(number * 100));
const params = [...fictiveSensor, 0x00, 0x01, 0x00, 0x55, ...temperatureBuf];
const data = [...lumiHeader(0x12, params.length, 0x05), ...params];
await entity.write("manuSpecificLumi", { 65522: { value: data, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
return { state: { external_temperature: value } };
}
break;
case "external_humidity":
if (meta.state.sensor === "external") {
const humidityBuf = node_buffer_1.Buffer.alloc(4);
const number = (0, utils_1.toNumber)(value);
humidityBuf.writeFloatBE(Math.round(number * 100));
const params = [...fictiveSensor, 0x00, 0x02, 0x00, 0x55, ...humidityBuf];
const data = [...lumiHeader(0x12, params.length, 0x05), ...params];
await entity.write("manuSpecificLumi", { 65522: { value: data, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
return { state: { external_humidity: value } };
}
break;
default: // Unknown key
logger_1.logger.debug(`Unhandled key ${key}`, "zhc:lumi:externalSensor");
}
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x172], { manufacturerCode: exports.manufacturerCode });
},
},
],
fromZigbee: [
{
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = {};
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
Object.entries(msg.data).forEach(([key, value]) => {
switch (Number.parseInt(key)) {
case 0x0172:
result.sensor = (0, utils_1.getFromLookup)(value, { 2: "external", 0: "internal", 1: "internal", 3: "external" });
break;
case 0xfff2:
logger_1.logger.debug(`Unhandled key ${key} = ${value}`, "zhc:lumi:externalSensor");
break;
default:
logger_1.logger.debug(`Unknown key ${key} = ${value}`, "zhc:lumi:externalSensor");
}
});
return result;
},
},
],
};
},
};
exports.modernExtend = exports.lumiModernExtend;
const feederDaysLookup = {
127: "everyday",
31: "workdays",
96: "weekend",
1: "mon",
2: "tue",
4: "wed",
8: "thu",
16: "fri",
32: "sat",
64: "sun",
85: "mon-wed-fri-sun",
42: "tue-thu-sat",
};
exports.fromZigbee = {
// lumi generic
lumi_basic: {
cluster: "genBasic",
type: ["attributeReport", "readResponse"],
convert: async (model, msg, publish, options, meta) => {
return await (0, exports.numericAttributes2Payload)(msg, meta, model, options, msg.data);
},
},
lumi_basic_raw: {
cluster: "genBasic",
type: ["raw"],
convert: async (model, msg, publish, options, meta) => {
let payload = {};
if (node_buffer_1.Buffer.isBuffer(msg.data)) {
const dataObject = (0, exports.buffer2DataObject)(model, msg.data);
payload = await (0, exports.numericAttributes2Payload)(msg, meta, model, options, dataObject);
}
return payload;
},
},
lumi_specific: {
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: async (model, msg, publish, options, meta) => {
return await (0, exports.numericAttributes2Payload)(msg, meta, model, options, msg.data);
},
},
lumi_co2: {
cluster: "msCO2",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
return { co2: Math.floor(msg.data.measuredValue) };
},
},
lumi_pm25: {
cluster: "pm25Measurement",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.measuredValue) {
return { pm25: msg.data.measuredValue };
}
},
},
lumi_contact: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
return { contact: msg.data.onOff === 0 };
},
},
lumi_power: {
cluster: "genAnalogInput",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
return { power: msg.data.presentValue };
},
},
lumi_action: {
cluster: "genOnOff",
type: ["attributeReport"],
convert: (model, msg, publish, options, meta) => {
if (["QBKG04LM", "QBKG11LM", "QBKG21LM", "QBKG03LM", "QBKG12LM", "QBKG22LM"].includes(model.model) && msg.data["61440"]) {
return;
}
if (model.model === "WXKG11LM") {
// biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
let clicks;
if (msg.data.onOff) {
clicks = 1;
}
else if (msg.data["32768"]) {
clicks = msg.data["32768"];
}
const actionLookup = { 1: "single", 2: "double", 3: "triple", 4: "quadruple" };
if (actionLookup[clicks]) {
return { action: actionLookup[clicks] };
}
return;
}
if (["QBKG21LM", "QBKG04LM"].includes(model.model) && msg.endpoint.ID !== 4)
return;
let mapping = null;
if (["QBKG03LM", "QBKG12LM", "QBKG22LM"].includes(model.model))
mapping = { 4: "left", 5: "right", 6: "both" };
if (["WXKG02LM_rev1", "WXKG02LM_rev2", "WXKG07LM"].includes(model.model))
mapping = { 1: "left", 2: "right", 3: "both" };
// Maybe other QKBG also support release/hold?
const actionLookup = ["QBKG03LM", "QBKG22LM", "QBKG04LM", "QBKG21LM"].includes(model.model)
? { 0: "hold", 1: "release", 2: "double" }
: { 0: "single", 1: "single" };
const action = actionLookup[msg.data.onOff];
const button = mapping?.[msg.endpoint.ID] ? `_${mapping[msg.endpoint.ID]}` : "";
if (action === "release") {
const anotherAction = globalStore.getValue(msg.endpoint, "hold", false) ? "hold_release" : "single";
publish({ action: `${anotherAction}${button}` });
}
globalStore.putValue(msg.endpoint, "hold", action === "hold");
return { action: `${action}${button}` };
},
},
lumi_action_multistate: {
cluster: "genMultistateInput",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
return;
// cubes
if (model.model === "MFKZQ01LM") {
/*
Source: https://github.com/kirovilya/ioBroker.zigbee
+---+
| 2 |
+---+---+---+
| 4 | 0 | 1 |
+---+---+---+
|M5I|
+---+
| 3 |
+---+
Side 5 is with the MI logo, side 3 contains the battery door.
presentValue = 0 = shake
presentValue = 2 = wakeup
presentValue = 3 = fly/fall
presentValue = y + x * 8 + 64 = 90º Flip from side x on top to side y on top
presentValue = x + 128 = 180º flip to side x on top
presentValue = x + 256 = push/slide cube while side x is on top
presentValue = x + 512 = double tap while side x is on top
*/
const value = msg.data.presentValue;
let result = null;
if (value === 0)
result = { action: "shake" };
else if (value === 1)
result = { action: "throw" };
else if (value === 2)
result = { action: "wakeup" };
else if (value === 3)
result = { action: "fall" };
else if (value >= 512)
result = { action: "tap", side: value - 512 };
else if (value >= 256)
result = { action: "slide", side: value - 256 };
else if (value >= 128)
result = { action: "flip180", side: value - 128 };
else if (value >= 64) {
result = {
action: "flip90",
action_from_side: Math.floor((value - 64) / 8),
action_to_side: value % 8,
action_side: value % 8,
from_side: Math.floor((value - 64) / 8),
to_side: value % 8,
side: value % 8,
};
}
return result ? result : null;
}
if (model.model === "CTP-R01") {
const value = msg.data.presentValue;
// biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
let payload;
if (value === 0)
payload = { action: "shake" };
else if (value === 1)
payload = { action: "throw" };
else if (value === 2)
payload = { action: "1_min_inactivity" };
else if (value === 4)
payload = { action: "hold" };
else if (value >= 1024)
payload = { action: "flip_to_side", side: value - 1023 };
else if (value >= 512)
payload = { action: "tap", side: value - 511 };
else if (value >= 256)
payload = { action: "slide", side: value - 255 };
else if (value >= 128) {
payload = {
action: "flip180",
side: value - 127,
action_from_side: 7 - value + 127,
};
}
else if (value >= 64) {
payload = {
action: "flip90",
side: (value % 8) + 1,
action_from_side: Math.floor((value - 64) / 8) + 1,
};
}
else {
logger_1.logger.debug(`${model.model}: unknown action with value ${value}`, NS);
}
return payload;
}
let actionLookup = { 0: "hold", 1: "single", 2: "double", 3: "triple", 255: "release" };
// mini switches and opple
if (model.model === "WXKG12LM") {
actionLookup = { ...actionLookup, 16: "hold", 17: "release", 18: "shake" };
}
if (["WXKG13LM", "WXKG04LM", "WXCJKG11LM", "WXCJKG12LM", "WXCJKG13LM"].includes(model.model)) {
actionLookup = { ...actionLookup, 5: "quintuple", 6: "many" };
}
// wall switches
let buttonLookup = null;
if (["WXKG02LM_rev2", "WXKG07LM", "WXKG15LM", "WXKG17LM", "WXKG22LM"].includes(model.model)) {
buttonLookup = { 1: "left", 2: "right", 3: "both" };
}
if (["QBKG12LM", "QBKG24LM"].includes(model.model))
buttonLookup = { 5: "left", 6: "right", 7: "both" };
if (["QBKG39LM", "QBKG41LM", "WS-EUK02", "WS-EUK04", "QBKG18LM", "QBKG20LM", "QBKG28LM", "QBKG31LM", "ZNQBKG25LM"].includes(model.model)) {
buttonLookup = { 41: "left", 42: "right", 51: "both" };
}
if (["QBKG25LM", "QBKG26LM", "QBKG29LM", "QBKG32LM", "QBKG33LM", "QBKG34LM", "ZNQBKG31LM", "ZNQBKG26LM"].includes(model.model)) {
buttonLookup = {
41: "left",
42: "center",
43: "right",
51: "left_center",
52: "left_right",
53: "center_right",
61: "all",
};
}
// Z1 switches, ZNQBKG38LM only 1 button, so not add buttonLookup
if (["ZNQBKG39LM"].includes(model.model)) {
buttonLookup = { 1: "top", 2: "bottom" };
}
if (["ZNQBKG40LM"].includes(model.model)) {
buttonLookup = { 1: "top", 2: "center", 3: "bottom" };
}
if (["ZNQBKG41LM"].includes(model.model)) {
buttonLookup = { 1: "top", 2: "center", 3: "bottom", 4: "wireless" };
}
if (["WS-USC02", "WS-USC04"].includes(model.model)) {
buttonLookup = { 41: "top", 42: "bottom", 51: "both" };
}
const action = actionLookup[msg.data.presentValue];
if (["WXKG04LM", "WXCJKG11LM", "WXCJKG12LM", "WXCJKG13LM"].includes(model.model)) {
clearTimeout(globalStore.getValue(msg.endpoint, "timer"));
// 0 = hold
const button = msg.endpoint.ID;
if (msg.data.presentValue === 0) {
// Aqara Opple does not generate a release event when pressed for more than 5 seconds
// After 5 seconds of not releasing we assume release.
const timer = setTimeout(() => publish({ action: `button_${button}_release` }), 5000);
globalStore.putValue(msg.endpoint, "timer", timer);
}
return { action: `button_${button}_${action}` };
}
if (buttonLookup) {
const button = buttonLookup[msg.endpoint.ID];
if (button) {
return { action: `${action}_${button}` };
}
}
else {
return { action };
}
},
},
lumi_action_analog: {
cluster: "genAnalogInput",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (model.model === "MFKZQ01LM") {
/*
Source: https://github.com/kirovilya/ioBroker.zigbee
presentValue = rotation angle left < 0, right > 0
*/
const value = msg.data.presentValue;
const result = {
action: value < 0 ? "rotate_left" : "rotate_right",
angle: Math.floor(value * 100) / 100,
action_angle: Math.floor(value * 100) / 100,
};
return result;
}
if (model.model === "CTP-R01") {
const value = msg.data.presentValue;
return {
action: value < 0 ? "rotate_left" : "rotate_right",
action_angle: Math.floor(value * 100) / 100,
};
}
},
},
lumi_temperature: {
cluster: "msTemperatureMeasurement",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const temperature = Number.parseFloat(msg.data.measuredValue) / 100.0;
// https://github.com/Koenkk/zigbee2mqtt/issues/798
// Sometimes the sensor publishes non-realistic vales.
if (temperature > -65 && temperature < 65) {
return { temperature };
}
},
},
lumi_pressure: {
cluster: "msPressureMeasurement",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = fz.pressure.convert(model, msg, publish, options, meta);
if (result && result.pressure > 500 && result.pressure < 2000) {
return result;
}
},
},
// lumi class specific
lumi_feeder: {
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = {};
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
Object.entries(msg.data).forEach(([key, value]) => {
switch (Number.parseInt(key)) {
case 0xfff1: {
// @ts-expect-error ignore
if (value.length < 8) {
logger_1.logger.debug(`Cannot handle ${value}, frame too small`, "zhc:lumi:feeder");
return;
}
// @ts-expect-error ignore
const attr = value.slice(3, 7);
// @ts-expect-error ignore
const len = value.slice(7, 8).readUInt8();
// @ts-expect-error ignore
const val = value.slice(8, 8 + len);
switch (attr.readInt32BE()) {
case 0x04150055: // feeding
result.feed = "";
break;
case 0x041502bc: {
// feeding report
const report = val.toString();
result.feeding_source = { 0: "schedule", 1: "manual", 2: "remote" }[Number.parseInt(report.slice(0, 2))];
result.feeding_size = Number.parseInt(report.slice(3, 4));
break;
}
case 0x0d680055: // portions per day
result.portions_per_day = val.readUInt16BE();
break;
case 0x0d690055: // weight per day
result.weight_per_day = val.readUInt32BE();
break;
case 0x0d0b0055: // error ?
result.error = (0, utils_1.getFromLookup)(val.readUInt8(), { 1: true, 0: false });
break;
case 0x080008c8: {
// schedule string
const schlist = val.toString().split(",");
const schedule = [];
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
schlist.forEach((str) => {
// 7f13000100
if (str !== "//") {
const feedtime = node_buffer_1.Buffer.from(str, "hex");
schedule.push({
days: (0, utils_1.getFromLookup)(feedtime[0], feederDaysLookup),
hour: feedtime[1],
minute: feedtime[2],
size: feedtime[3],
});
}
});
result.schedule = schedule;
break;
}
case 0x04170055: // indicator
result.led_indicator = (0, utils_1.getFromLookup)(val.readUInt8(), { 1: "ON", 0: "OFF" });
break;
case 0x04160055: // child lock
result.child_lock = (0, utils_1.getFromLookup)(val.readUInt8(), { 1: "LOCK", 0: "UNLOCK" });
break;
case 0x04180055: // mode
result.mode = (0, utils_1.getFromLookup)(val.readUInt8(), { 1: "schedule", 0: "manual" });
break;
case 0x0e5c0055: // serving size
result.serving_size = val.readUInt8();
break;
case 0x0e5f0055: // portion weight
result.portion_weight = val.readUInt8();
break;
case 0x080007d1: // ? 64
case 0x0d090055: // ? 00
logger_1.logger.debug(`Unhandled attribute ${attr} = ${val}`, "zhc:lumi:feeder");
break;
default:
logger_1.logger.debug(`Unknown attribute ${attr} = ${val}`, "zhc:lumi:feeder");
}
break;
}
case 0x00ff: // 80:13:58:91:24:33:20:24:58:53:44:07:05:97:75:17
case 0x0007: // 00:00:00:00:1d:b5:a6:ed
case 0x00f7: // 05:21:14:00:0d:23:21:25:00:00:09:21:00:01
logger_1.logger.debug(`Unhandled key ${key} = ${value}`, "zhc:lumi:feeder");
break;
default:
logger_1.logger.debug(`Unknown key ${key} = ${value}`, "zhc:lumi:feeder");
}
});
return result;
},
},
lumi_trv: {
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = {};
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
Object.entries(msg.data).forEach(([key, value]) => {
switch (Number.parseInt(key)) {
case 0x0271:
result.system_mode = (0, utils_1.getFromLookup)(value, { 1: "heat", 0: "off" });
break;
case 0x0272:
// @ts-expect-error ignore
Object.assign(result, exports.trv.decodePreset(value));
break;
case 0x0273:
result.window_detection = (0, utils_1.getFromLookup)(value, { 1: true, 0: false });
break;
case 0x0274:
result.valve_detection = (0, utils_1.getFromLookup)(value, { 1: true, 0: false });
break;
case 0x0277:
result.child_lock = (0, utils_1.getFromLookup)(value, { 1: "LOCK", 0: "UNLOCK" });
break;
case 0x0279:
(0, utils_1.assertNumber)(value);
result.away_preset_temperature = (value / 100).toFixed(1);
break;
case 0x027b:
result.calibrated = (0, utils_1.getFromLookup)(value, { 1: true, 0: false });
break;
case 0x027e:
result.sensor = (0, utils_1.getFromLookup)(value, { 1: "external", 0: "internal", 2: "external" });
break;
case 0x040a:
result.battery = value;
break;
case 0x027a:
result.window_open = (0, utils_1.getFromLookup)(value, { 1: true, 0: false });
break;
case 0x0275:
result.valve_alarm = (0, utils_1.getFromLookup)(value, { 1: true, 0: false, 4: true });
break;
case 247: {
// @ts-expect-error ignore
const heartbeat = exports.trv.decodeHeartbeat(meta, model, value);
logger_1.logger.debug(`${model.model}: Processed heartbeat message into payload ${JSON.stringify(heartbeat)}`, "zhc:lumi:trv");
if (heartbeat.firmware_version) {
// Overwrite the "placeholder" version `0.0.0_0025` advertised by `genBasic`
// with the correct version from the heartbeat.
// This is not reflected in the frontend unless the device is reconfigured
// or the whole service restarted.
// See https://github.com/Koenkk/zigbee-herdsman-converters/pull/5363#discussion_r1081477047
// @ts-expect-error ignore
meta.device.softwareBuildID = heartbeat.firmware_version;
// biome-ignore lint/performance/noDelete: ignored using `--suppress`
delete heartbeat.firmware_version;
}
Object.assign(result, heartbeat);
break;
}
case 0x027d:
result.schedule = (0, utils_1.getFromLookup)(value, { 1: true, 0: false });
break;
case 0x0276: {
const buffer = value;
// Buffer is empty first message after pairing
// https://github.com/Koenkk/zigbee-herdsman-converters/issues/7128
if (buffer.length) {
const schedule = exports.trv.decodeSchedule(buffer);
result.schedule_settings = exports.trv.stringifySchedule(schedule);
}
break;
}
case 0x00ee: {
meta.device.meta.lumiFileVersion = value;
meta.device.save();
break;
}
case 0xfff2:
case 0x00ff: // 4e:27:49:bb:24:b6:30:dd:74:de:53:76:89:44:c4:81
case 0x027c: // 0x00
case 0x0280: // 0x00/0x01
logger_1.logger.debug(`Unhandled key ${key} = ${value}`, "zhc:lumi:trv");
break;
default:
logger_1.logger.debug(`Unknown key ${key} = ${value}`, "zhc:lumi:trv");
}
});
return result;
},
},
lumi_presence_region_events: {
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const payload = {};
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
Object.entries(msg.data).forEach(([key, value]) => {
const eventKey = Number.parseInt(key);
switch (eventKey) {
case exports.presence.constants.region_event_key: {
if (!node_buffer_1.Buffer.isBuffer(value) ||
!(typeof value[0] === "string" || typeof value[0] === "number") ||
!(typeof value[1] === "string" || typeof value[1] === "number")) {
logger_1.logger.warning(`Action: Unrecognized payload structure '${JSON.stringify(value)}'`, NS);
break;
}
const [regionIdRaw, eventTypeCodeRaw] = value;
// @ts-expect-error ignore
const regionId = Number.parseInt(regionIdRaw, 10);
// @ts-expect-error ignore
const eventTypeCode = Number.parseInt(eventTypeCodeRaw, 10);
if (Number.isNaN(regionId)) {
logger_1.logger.warning(`Action: Invalid regionId "${regionIdRaw}"`, NS);
break;
}
if (!Object.values(exports.presence.constants.region_event_types).includes(eventTypeCode)) {
logger_1.logger.warning(`Action: Unknown region event type "${eventTypeCode}"`, NS);
break;
}
const eventTypeName = exports.presence.mappers.lumi_presence.region_event_type_names[eventTypeCode];
logger_1.logger.debug(`Action: Triggered event (region "${regionId}", type "${eventTypeName}")`, NS);
payload.action = `region_${regionId}_${eventTypeName}`;
break;
}
}
});
return payload;
},
},
lumi_lock_report: {
cluster: "genBasic",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data["65328"]) {
const data = `0x${msg.data["65328"].toString(16)}`;
const state = Number(data.substring(2, 4));
const action = Number(data.substring(4, 6));
const keynum = Number(data.substring(6, 8));
if (state === 11) {
if (action === 1) {
// unknown key
return { keyerror: true, inserted: "unknown" };
}
if (action === 3) {
// explicitly disabled key (i.e. reported lost)
return { keyerror: true, inserted: keynum };
}
if (action === 7) {
// strange object introduced into the cylinder (e.g. a lock pick)
return { keyerror: true, inserted: "strange" };
}
}
if (state === 12) {
if (action === 1) {
return { inserted: keynum };
}
if (action === 11) {
return { forgotten: keynum };
}
}
}
},
},
lumi_occupancy_illuminance: {
// This is for occupancy sensor that only send a message when motion detected,
// but do not send a motion stop.
// Therefore we need to publish the no_motion detected by ourselves.
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
options: [exposes.options.occupancy_timeout_2(), exposes.options.no_occupancy_since_true()],
convert: (model, msg, publish, options, meta) => {
if (msg.data.illuminance !== undefined) {
// The occupancy sensor only sends a message when motion detected.
// Therefore we need to publish the no_motion detected by ourselves.
let timeout = meta && meta.state && meta.state.detection_interval !== undefined
? Number(meta.state.detection_interval)
: ["RTCGQ14LM"].includes(model.model)
? 30
: 60;
timeout =
options?.occupancy_timeout != null && Number(options.occupancy_timeout) >= timeout
? Number(options.occupancy_timeout)
: timeout + 2;
// Stop existing timers because motion is detected and set a new one.
clearTimeout(globalStore.getValue(msg.endpoint, "occupancy_timer", null));
if (timeout !== 0) {
const timer = setTimeout(() => {
publish({ occupancy: false });
}, timeout * 1000);
globalStore.putValue(msg.endpoint, "occupancy_timer", timer);
}
// Sometimes RTCGQ14LM reports high illuminance values in the dark
// https://github.com/Koenkk/zigbee2mqtt/issues/12596
const illuminance = msg.data.illuminance > 130536 ? 0 : msg.data.illuminance - 65536;
const payload = { occupancy: true, illuminance };
(0, utils_1.noOccupancySince)(msg.endpoint, options, publish, "start");
return payload;
}
},
},
lumi_curtain_position: {
cluster: "genAnalogOutput",
type: ["attributeReport", "readResponse"],
options: [exposes.options.invert_cover()],
convert: (model, msg, publish, options, meta) => {
if (model.model === "ZNCLDJ12LM" && msg.type === "attributeReport" && [0, 2].includes(msg.data.presentValue)) {
// Incorrect reports from the device, ignore (re-read by onEvent of ZNCLDJ12LM)
// https://github.com/Koenkk/zigbee-herdsman-converters/pull/1427#issuecomment-663862724
return;
}
let position = (0, utils_1.precisionRound)(msg.data.presentValue, 2);
position = options.invert_cover ? 100 - position : position;
const closed = options.invert_cover ? position === 100 : position === 0;
return { position, state: closed ? "CLOSE" : "OPEN" };
},
},
lumi_curtain_position_tilt: {
cluster: "closuresWindowCovering",
type: ["attributeReport", "readResponse"],
options: [exposes.options.invert_cover()],
convert: (model, msg, publish, options, meta) => {
const result = {};
const invert = model.meta?.coverInverted ? !options.invert_cover : options.invert_cover;
if (msg.data.currentPositionLiftPercentage !== undefined && msg.data.currentPositionLiftPercentage <= 100) {
const value = msg.data.currentPositionLiftPercentage;
const position = invert ? 100 - value : value;
const state = invert ? (position > 0 ? "CLOSE" : "OPEN") : position > 0 ? "OPEN" : "CLOSE";
result[(0, utils_1.postfixWithEndpointName)("position", msg, model, meta)] = position;
result[(0, utils_1.postfixWithEndpointName)("state", msg, model, meta)] = state;
}
if (msg.data.currentPositionTiltPercentage !== undefined && msg.data.currentPositionTiltPercentage <= 100) {
const value = msg.data.currentPositionTiltPercentage;
result[(0, utils_1.postfixWithEndpointName)("tilt", msg, model, meta)] = invert ? 100 - value : value;
}
return result;
},
},
lumi_operation_mode_basic: {
cluster: "genBasic",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const payload = {};
if (model.meta && !model.meta.multiEndpoint) {
const mappingMode = { 18: "control_relay", 254: "decoupled" };
const key = 0xff22;
if (msg.data[key] !== undefined) {
payload.operation_mode = mappingMode[msg.data[key]];
}
}
else {
const mappingButton = { 65314: "left", 65315: "right" };
const mappingMode = { 18: "control_left_relay", 34: "control_right_relay", 254: "decoupled" };
for (const key in mappingButton) {
if (msg.data[key] !== undefined) {
payload[`operation_mode_${mappingButton[key]}`] = mappingMode[msg.data[key]];
}
}
}
return payload;
},
},
lumi_bulb_interval: {
cluster: "genBasic",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data["65281"]) {
const data = msg.data["65281"];
return {
state: data["100"] === 1 ? "ON" : "OFF",
brightness: data["101"],
color_temp: data["102"],
};
}
},
},
lumi_on_off: {
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
// Lumi wall switches use endpoint 4, 5 or 6 to indicate an action on the button so we have to skip that.
if (msg.data.onOff !== undefined && ![4, 5, 6].includes(msg.endpoint.ID)) {
const property = (0, utils_1.postfixWithEndpointName)("state", msg, model, meta);
return { [property]: msg.data.onOff === 1 ? "ON" : "OFF" };
}
},
},
lumi_curtain_status: {
cluster: "genMultistateOutput",
type: ["attributeReport"],
convert: (model, msg, publish, options, meta) => {
let running = false;
const data = msg.data;
let lookup = {};
// For lumi.curtain.hagl04 and lumi.curtain.hagl07
if (["ZNCLDJ12LM", "ZNCLDJ14LM"].includes(model.model))
lookup = { 0: "closing", 1: "opening", 2: "stopped" };
// for lumi.curtain.acn002
if (["ZNJLBL01LM"].includes(model.model))
lookup = { 0: "closing", 1: "opening", 2: "stopped", 3: "blocked" };
if (data && data.presentValue !== undefined) {
const value = data.presentValue;
if (value < 2) {
running = true;
}
return {
motor_state: lookup[value],
running: running,
};
}
},
},
lumi_curtain_options: {
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.curtainHandOpen !== undefined) {
return { hand_open: msg.data.curtainHandOpen === 0 };
}
if (msg.data.curtainReverse !== undefined) {
return { reverse_direction: msg.data.curtainReverse === 1 };
}
if (msg.data.curtainCalibrated !== undefined) {
return { limits_calibration: msg.data.curtainCalibrated === 1 ? "calibrated" : "recalibrate" };
}
},
},
lumi_vibration_analog: {
cluster: "closuresDoorLock",
type: ["attributeReport", "readResponse"],
options: [
exposes.options.vibration_timeout(),
exposes.options.calibration("x"),
exposes.options.calibration("y"),
exposes.options.calibration("z"),
],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data["85"]) {
const vibrationLookup = { 1: "vibration", 2: "tilt", 3: "drop" };
result.action = vibrationLookup[msg.data["85"]];
// Device only sends a message when vibration is detected.
// Therefore we need to publish a no_vibration message on our own.
if (result.action === "vibration") {
result.vibration = true;
const timeout = options?.vibration_timeout != null ? Number(options.vibration_timeout) : 90;
// Stop any existing timer cause vibration detected
clearTimeout(globalStore.getValue(msg.endpoint, "vibration_timer", null));
globalStore.putValue(msg.endpoint, "vibration_timer", null);
// Set new timer to publish no_vibration message
if (timeout !== 0) {
const timer = setTimeout(() => {
publish({ vibration: false });
}, timeout * 1000);
globalStore.putValue(msg.endpoint, "vibration_timer", timer);
}
}
}
if (msg.data["1283"]) {
result.angle = msg.data["1283"];
}
if (msg.data["1285"]) {
// https://github.com/dresden-elektronik/deconz-rest-plugin/issues/748#issuecomment-419669995
// Only first 2 bytes are relevant.
const data = msg.data["1285"] >> 8;
// Swap byte order
result.strength = ((data & 0xff) << 8) | ((data >> 8) & 0xff);
}
if (msg.data["1288"]) {
const data = msg.data["1288"];
const buffer = node_buffer_1.Buffer.alloc(6);
buffer.writeUIntLE(data, 0, 6);
let x = buffer.readIntLE(0, 2);
let y = buffer.readIntLE(2, 2);
let z = buffer.readIntLE(4, 2);
// simple offset calibration
x = (0, utils_1.calibrateAndPrecisionRoundOptions)(x, options, "x");
y = (0, utils_1.calibrateAndPrecisionRoundOptions)(y, options, "y");
z = (0, utils_1.calibrateAndPrecisionRoundOptions)(z, options, "z");
// calibrated accelerometer values
result.x_axis = x;
result.y_axis = y;
result.z_axis = z;
// calculate angle
result.angle_x = Math.round((Math.atan(x / Math.sqrt(y * y + z * z)) * 180) / Math.PI);
result.angle_y = Math.round((Math.atan(y / Math.sqrt(x * x + z * z)) * 180) / Math.PI);
result.angle_z = Math.round((Math.atan(z / Math.sqrt(x * x + y * y)) * 180) / Math.PI);
// calculate absolute angle
const R = Math.sqrt(x * x + y * y + z * z);
result.angle_x_absolute = Math.round((Math.acos(x / R) * 180) / Math.PI);
result.angle_y_absolute = Math.round((Math.acos(y / R) * 180) / Math.PI);
}
return result;
},
},
lumi_illuminance: {
cluster: "msIlluminanceMeasurement",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
// also trigger movement, because there is no illuminance without movement
// https://github.com/Koenkk/zigbee-herdsman-converters/issues/1925
msg.data.occupancy = 1;
const payload = fz.occupancy_with_timeout.convert(model, msg, publish, options, meta);
if (payload) {
const illuminance = msg.data.measuredValue;
payload.illuminance = illuminance;
}
return payload;
},
},
lumi_occupancy: {
// This is for occupancy sensor that only send a message when motion detected,
// but do not send a motion stop.
// Therefore we need to publish the no_motion detected by ourselves.
cluster: "msOccupancySensing",
type: ["attributeReport", "readResponse"],
options: [exposes.options.occupancy_timeout_2(), exposes.options.no_occupancy_since_true()],
convert: (model, msg, publish, options, meta) => {
if (msg.data.occupancy !== 1) {
// In case of 0 no occupancy is reported.
// https://github.com/Koenkk/zigbee2mqtt/issues/467
return;
}
// The occupancy sensor only sends a message when motion detected.
// Therefore we need to publish the no_motion detected by ourselves.
let timeout = meta && meta.state && meta.state.detection_interval !== undefined ? Number(meta.state.detection_interval) : 60;
timeout =
options?.occupancy_timeout != null && Number(options.occupancy_timeout) >= timeout ? Number(options.occupancy_timeout) : timeout + 2;
// Stop existing timers because motion is detected and set a new one.
clearTimeout(globalStore.getValue(msg.endpoint, "occupancy_timer", null));
if (timeout !== 0) {
const timer = setTimeout(() => {
publish({ occupancy: false });
}, timeout * 1000);
globalStore.putValue(msg.endpoint, "occupancy_timer", timer);
}
const payload = { occupancy: true };
(0, utils_1.noOccupancySince)(msg.endpoint, options, publish, "start");
return payload;
},
},
lumi_smoke: {
cluster: "ssIasZone",
type: "commandStatusChangeNotification",
convert: (model, msg, publish, options, meta) => {
const result = fz.ias_smoke_alarm_1.convert(model, msg, publish, options, meta);
const zoneStatus = msg.data.zonestatus;
if (result)
result.test = (zoneStatus & (1 << 1)) > 0;
return result;
},
},
lumi_gas_density: {
cluster: "genBasic",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const data = msg.data;
if (data?.["65281"]) {
const basicAttrs = data["65281"];
if (basicAttrs["100"] !== undefined) {
return { gas_density: basicAttrs["100"] };
}
}
},
},
lumi_gas_sensitivity: {
cluster: "ssIasZone",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const data = msg.data;
const lookup = { "1": "low", "2": "medium", "3": "high" };
if (data && data["65520"] !== undefined) {
const value = data["65520"];
if (value?.startsWith("0x020")) {
return {
sensitivity: lookup[value.charAt(5)],
};
}
}
},
},
lumi_door_lock_low_battery: {
cluster: "genPowerCfg",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
if (msg.data.batteryAlarmMask !== undefined) {
return { battery_low: msg.data.batteryAlarmMask === 1 };
}
},
},
lumi_door_lock_report: {
cluster: "closuresDoorLock",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = {};
const lockStatusLookup = {
1: "finger_not_match",
2: "password_not_match",
3: "reverse_lock", // disable open from outside
4: "reverse_lock_cancel", // enable open from outside
5: "locked",
6: "lock_opened",
7: "finger_add",
8: "finger_delete",
9: "password_add",
10: "password_delete",
11: "lock_opened_inside", // Open form inside reverse lock enable
12: "lock_opened_outside", // Open form outside reverse lock disable
13: "ring_bell",
14: "change_language_to",
15: "finger_open",
16: "password_open",
17: "door_closed",
};
if (model.model === "ZNMS11LM") {
if (msg.data["65296"]) {
// finger/password success
const data = msg.data["65296"].toString(16);
const command = data.substr(0, 1); // 1 finger open, 2 password open
const userId = data.substr(5, 2);
const userType = data.substr(1, 1); // 1 admin, 2 user
result.data = data;
result.action = `${lockStatusLookup[14 + Number.parseInt(command, 16)] + (userType === "1" ? "_admin" : "_user")}_id${Number.parseInt(userId, 16).toString()}`;
result.action_user = Number.parseInt(userId, 16);
}
else if (msg.data["65297"]) {
// finger, password failed or bell
const data = msg.data["65297"].toString(16);
const times = data.substr(0, 1);
const type = data.substr(5, 2); // 00 bell, 02 password, 40 error finger
result.data = data;
if (type === "40") {
result.action_action = lockStatusLookup[1];
result.action_repeat = Number.parseInt(times, 16);
}
else if (type === "02") {
result.action = lockStatusLookup[2];
result.action_repeat = Number.parseInt(times, 16);
}
else if (type === "00") {
result.action = lockStatusLookup[13];
}
}
else if (msg.data["65281"]?.["1"]) {
// user added/delete
const data = msg.data["65281"]["1"].toString(16);
const command = data.substr(0, 1); // 1 add, 2 delete
const userId = data.substr(5, 2);
result.action = lockStatusLookup[6 + Number.parseInt(command, 16)];
result.action_user = Number.parseInt(userId, 16);
}
}
if (["ZNMS12LM", "ZNMS13LM"].includes(model.model)) {
if (msg.data["65526"]) {
// lock final status
// Convert data back to hex to decode
const data = node_buffer_1.Buffer.from(msg.data["65526"], "ascii").toString("hex");
const command = data.substr(6, 4);
if (command === "0301" || // ZNMS12LM
command === "0341" // ZNMS13LM
) {
result.action = lockStatusLookup[4];
result.state = "UNLOCK";
result.reverse = "UNLOCK";
}
else if (command === "0311" || // ZNMS12LM
command === "0351" // ZNMS13LM
) {
result.action = lockStatusLookup[4];
result.state = "LOCK";
result.reverse = "UNLOCK";
}
else if (command === "0205" || // ZNMS12LM
command === "0245" // ZNMS13LM
) {
result.action = lockStatusLookup[3];
result.state = "UNLOCK";
result.reverse = "LOCK";
}
else if (command === "0215" || // ZNMS12LM
command === "0255" || // ZNMS13LM
command === "1355" // ZNMS13LM
) {
result.action = lockStatusLookup[3];
result.state = "LOCK";
result.reverse = "LOCK";
}
else if (command === "0111" || // ZNMS12LM
command === "1351" || // ZNMS13LM locked from inside
command === "1451" // ZNMS13LM locked from outside
) {
result.action = lockStatusLookup[5];
result.state = "LOCK";
result.reverse = "UNLOCK";
}
else if (command === "0b00" || // ZNMS12LM
command === "0640" || // ZNMS13LM
command === "0600" // ZNMS13LM
) {
result.action = lockStatusLookup[12];
result.state = "UNLOCK";
result.reverse = "UNLOCK";
}
else if (command === "0c00" || // ZNMS12LM
command === "2300" || // ZNMS13LM
command === "0540" || // ZNMS13LM
command === "0440" // ZNMS13LM
) {
result.action = lockStatusLookup[11];
result.state = "UNLOCK";
result.reverse = "UNLOCK";
}
else if (command === "2400" || // ZNMS13LM door closed from insed
command === "2401" // ZNMS13LM door closed from outside
) {
result.action = lockStatusLookup[17];
result.state = "UNLOCK";
result.reverse = "UNLOCK";
}
}
else if (msg.data["65296"]) {
// finger/password success
const data = node_buffer_1.Buffer.from(msg.data["65296"], "ascii").toString("hex");
const command = data.substr(6, 2); // 1 finger open, 2 password open
const userId = data.substr(12, 2);
const userType = data.substr(8, 1); // 1 admin, 2 user
result.action = `${lockStatusLookup[14 + Number.parseInt(command, 16)] + (userType === "1" ? "_admin" : "_user")}_id${Number.parseInt(userId, 16).toString()}`;
result.action_user = Number.parseInt(userId, 16);
}
else if (msg.data["65297"]) {
// finger, password failed or bell
const data = node_buffer_1.Buffer.from(msg.data["65297"], "ascii").toString("hex");
const times = data.substr(6, 2);
const type = data.substr(12, 2); // 00 bell, 02 password, 40 error finger
if (type === "40") {
result.action = lockStatusLookup[1];
result.action_repeat = Number.parseInt(times, 16);
}
else if (type === "00") {
result.action = lockStatusLookup[13];
result.action_repeat = null;
}
else if (type === "02") {
result.action = lockStatusLookup[2];
result.action_repeat = Number.parseInt(times, 16);
}
}
else if (msg.data["65281"]) {
// password added/delete
const data = node_buffer_1.Buffer.from(msg.data["65281"], "ascii").toString("hex");
const command = data.substr(18, 2); // 1 add, 2 delete
const userId = data.substr(12, 2);
result.action = lockStatusLookup[6 + Number.parseInt(command, 16)];
result.action_user = Number.parseInt(userId, 16);
}
else if (msg.data["65522"]) {
// set language
const data = node_buffer_1.Buffer.from(msg.data["65522"], "ascii").toString("hex");
const langId = data.substr(6, 2); // 1 chinese, 2: english
result.action = lockStatusLookup[14] + (langId === "2" ? "_english" : "_chinese");
}
}
return result;
},
},
lumi_action_on: {
cluster: "genOnOff",
type: "commandOn",
convert: (model, msg, publish, options, meta) => {
if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
return;
return { action: "button_2_single" };
},
},
lumi_action_off: {
cluster: "genOnOff",
type: "commandOff",
convert: (model, msg, publish, options, meta) => {
if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
return;
return { action: "button_1_single" };
},
},
lumi_action_step: {
cluster: "genLevelCtrl",
type: "commandStep",
convert: (model, msg, publish, options, meta) => {
if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
return;
const button = msg.data.stepmode === 0 ? "4" : "3";
return { action: `button_${button}_single` };
},
},
lumi_action_stop: {
cluster: "genLevelCtrl",
type: "commandStop",
convert: (model, msg, publish, options, meta) => {
if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
return;
if (globalStore.hasValue(msg.endpoint, "button")) {
const value = globalStore.getValue(msg.endpoint, "button");
const duration = Date.now() - value.start;
const payload = { action: `button_${value.button}_release`, action_duration: duration };
return payload;
}
},
},
lumi_action_move: {
cluster: "genLevelCtrl",
type: "commandMove",
convert: (model, msg, publish, options, meta) => {
if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
return;
const button = msg.data.movemode === 0 ? "4" : "3";
globalStore.putValue(msg.endpoint, "button", { button, start: Date.now() });
return { action: `button_${button}_hold` };
},
},
lumi_action_step_color_temp: {
cluster: "lightingColorCtrl",
type: "commandStepColorTemp",
convert: (model, msg, publish, options, meta) => {
if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
return;
// biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
let action;
if (model.model === "WXCJKG12LM") {
// for WXCJKG12LM model it's double click event on buttons 3 and 4
action = msg.data.stepmode === 1 ? "3_double" : "4_double";
}
else {
// but for WXCJKG13LM model it's single click event on buttons 5 and 6
action = msg.data.stepmode === 1 ? "5_single" : "6_single";
}
return { action: `button_${action}` };
},
},
lumi_action_move_color_temp: {
cluster: "lightingColorCtrl",
type: "commandMoveColorTemp",
convert: (model, msg, publish, options, meta) => {
if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
return;
const stop = msg.data.movemode === 0;
let result = null;
if (stop) {
const button = globalStore.getValue(msg.endpoint, "button").button;
const duration = Date.now() - globalStore.getValue(msg.endpoint, "button").start;
result = { action: `button_${button}_release`, duration, action_duration: duration };
}
else {
const button = msg.data.movemode === 3 ? "6" : "5";
result = { action: `button_${button}_hold` };
globalStore.putValue(msg.endpoint, "button", { button, start: Date.now() });
}
return result;
},
},
// lumi device specific
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
lumi_action_WXKG01LM: {
// Unique converter
cluster: "genOnOff",
type: ["attributeReport", "readResponse"],
options: [
e
.numeric("hold_timeout", ea.SET)
.withValueMin(0)
.withDescription("The WXKG01LM only reports a button press and release." +
"By default, a hold action is published when there is at least 1000 ms between both events. It could be that due to " +
"delays in the network the release message is received late. This causes a single click to be identified as a hold " +
"action. If you are experiencing this you can try experimenting with this option (e.g. set it to 2000) (value is in ms)."),
e
.numeric("hold_timeout_expire", ea.SET)
.withValueMin(0)
.withDescription("Sometimes it happens that the button does not send a " +
"release. To avoid problems a release is automatically send after a timeout. The default timeout is 4000 ms, you can " +
"increase it with this option (value is in ms)."),
],
convert: (model, msg, publish, options, meta) => {
if ((0, utils_1.hasAlreadyProcessedMessage)(msg, model))
return;
const state = msg.data.onOff;
// 0 = click down, 1 = click up, else = multiple clicks
if (state === 0) {
const timer = setTimeout(() => {
publish({ action: "hold" });
globalStore.putValue(msg.endpoint, "timer", null);
globalStore.putValue(msg.endpoint, "hold", Date.now());
const holdTimer = setTimeout(() => {
globalStore.putValue(msg.endpoint, "hold", false);
}, options.hold_timeout_expire || 4000);
globalStore.putValue(msg.endpoint, "hold_timer", holdTimer);
// After 4000 milliseconds of not receiving release we assume it will not happen.
}, options.hold_timeout || 1000); // After 1000 milliseconds of not releasing we assume hold.
globalStore.putValue(msg.endpoint, "timer", timer);
}
else if (state === 1) {
if (globalStore.getValue(msg.endpoint, "hold")) {
const duration = Date.now() - globalStore.getValue(msg.endpoint, "hold");
publish({ action: "release", duration: duration });
globalStore.putValue(msg.endpoint, "hold", false);
}
if (globalStore.getValue(msg.endpoint, "timer")) {
clearTimeout(globalStore.getValue(msg.endpoint, "timer"));
globalStore.putValue(msg.endpoint, "timer", null);
publish({ action: "single" });
}
}
else {
const clicks = msg.data["32768"];
const actionLookup = { 1: "single", 2: "double", 3: "triple", 4: "quadruple" };
const payload = actionLookup[clicks] ? actionLookup[clicks] : "many";
publish({ action: payload });
}
},
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
lumi_smart_panel_ZNCJMB14LM: {
cluster: "manuSpecificLumi",
type: ["attributeReport", "readResponse"],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data[0x0215] !== undefined) {
const lookup = { 0: "classic", 1: "concise" };
result.theme = lookup[msg.data[0x0215]];
}
if (msg.data[0x0214] !== undefined) {
const lookup = { 1: "classic", 2: "analog clock" };
result.screen_saver_style = lookup[msg.data[0x0214]];
}
if (msg.data[0x0213] !== undefined) {
result.standby_enabled = !!(msg.data[0x0213] & 1);
}
if (msg.data[0x0212] !== undefined) {
const lookup = { 0: "mute", 1: "low", 2: "medium", 3: "high" };
result.beep_volume = lookup[msg.data[0x0212]];
}
if (msg.data[0x0211] !== undefined) {
result.lcd_brightness = msg.data[0x0211];
}
if (msg.data[0x022b] !== undefined) {
const lookup = { 0: "none", 1: "1", 2: "2", 3: "1 and 2", 4: "3", 5: "1 and 3", 6: "2 and 3", 7: "all" };
result.available_switches = lookup[msg.data[0x022b]];
}
if (msg.data[0x217] !== undefined) {
const lookup = { 3: "small", 4: "medium", 5: "large" };
result.font_size = lookup[msg.data[0x217]];
}
if (msg.data[0x219] !== undefined) {
const lookup = { 0: "scene", 1: "feel", 2: "thermostat", 3: "switch" };
result.homepage = lookup[msg.data[0x219]];
}
if (msg.data[0x210] !== undefined) {
const lookup = { 0: "chinese", 1: "english" };
result.language = lookup[msg.data[0x210]];
}
if (msg.data[0x216] !== undefined) {
result.standby_time = msg.data[0x216];
}
if (msg.data[0x218] !== undefined) {
result.lcd_auto_brightness_enabled = !!(msg.data[0x218] & 1);
}
if (msg.data[0x221] !== undefined) {
result.screen_saver_enabled = !!(msg.data[0x221] & 1);
}
if (msg.data[0x222] !== undefined) {
result.standby_lcd_brightness = msg.data[0x222];
}
if (msg.data[0x223] !== undefined) {
const lookup = { 1: "1", 2: "2", 3: "3", 4: "4", 5: "5", 6: "6", 7: "7", 8: "8", 9: "9", 10: "10", 11: "11" };
const textarr = msg.data[0x223].slice(1, msg.data[0x223].length);
result.switch_1_icon = lookup[msg.data[0x223][0]];
result.switch_1_text = String.fromCharCode(...textarr);
}
if (msg.data[0x224] !== undefined) {
const lookup = { 1: "1", 2: "2", 3: "3", 4: "4", 5: "5", 6: "6", 7: "7", 8: "8", 9: "9", 10: "10", 11: "11" };
const textarr = msg.data[0x224].slice(1, msg.data[0x224].length);
result.switch_2_icon = lookup[msg.data[0x224][0]];
result.switch_2_text = String.fromCharCode(...textarr);
}
if (msg.data[0x225] !== undefined) {
const lookup = { 1: "1", 2: "2", 3: "3", 4: "4", 5: "5", 6: "6", 7: "7", 8: "8", 9: "9", 10: "10", 11: "11" };
const textarr = msg.data[0x225].slice(1, msg.data[0x225].length);
result.switch_3_icon = lookup[msg.data[0x225][0]];
result.switch_3_text = String.fromCharCode(...textarr);
}
return result;
},
},
};
exports.toZigbee = {
// lumi generic
lumi_power: {
key: ["power"],
convertGet: async (entity, key, meta) => {
const endpoint = meta.device.endpoints.find((e) => e.supportsInputCluster("genAnalogInput")) ?? entity;
await endpoint.read("genAnalogInput", ["presentValue"]);
},
},
lumi_led_disabled_night: {
key: ["led_disabled_night"],
convertSet: async (entity, key, value, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if ([
"ZNCZ04LM",
"ZNCZ12LM",
"ZNCZ15LM",
"QBCZ14LM",
"QBCZ15LM",
"QBKG17LM",
"QBKG18LM",
"QBKG19LM",
"QBKG20LM",
"QBKG25LM",
"QBKG26LM",
"QBKG27LM",
"QBKG28LM",
"QBKG29LM",
"QBKG30LM",
"QBKG31LM",
"QBKG32LM",
"QBKG33LM",
"QBKG34LM",
"DLKZMK11LM",
"SSM-U01",
"WS-EUK01",
"WS-EUK02",
"WS-EUK03",
"WS-EUK04",
"SP-EUC01",
"ZNQBKG24LM",
"ZNQBKG25LM",
"ZNQBKG26LM",
"ZNQBKG38LM",
"ZNQBKG39LM",
"ZNQBKG40LM",
"ZNQBKG41LM",
].includes(meta.mapped.model)) {
await entity.write("manuSpecificLumi", { 515: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.lumi);
}
else if (["ZNCZ11LM"].includes(meta.mapped.model)) {
const payload = value
? [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x00, 0x03, 0x10, 0x00]
: [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x01, 0x03, 0x10, 0x01];
await entity.write("genBasic", { 65520: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
}
else {
throw new Error("Not supported");
}
return { state: { led_disabled_night: value } };
},
convertGet: async (entity, key, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if ([
"ZNCZ04LM",
"ZNCZ12LM",
"ZNCZ15LM",
"QBCZ15LM",
"QBCZ14LM",
"QBKG17LM",
"QBKG18LM",
"QBKG19LM",
"QBKG20LM",
"QBKG25LM",
"QBKG26LM",
"QBKG27LM",
"QBKG28LM",
"QBKG29LM",
"QBKG30LM",
"QBKG31LM",
"QBKG32LM",
"QBKG33LM",
"QBKG34LM",
"DLKZMK11LM",
"SSM-U01",
"WS-EUK01",
"WS-EUK02",
"WS-EUK03",
"WS-EUK04",
"SP-EUC01",
"ZNQBKG24LM",
"ZNQBKG25LM",
"ZNQBKG26LM",
"ZNQBKG38LM",
"ZNQBKG39LM",
"ZNQBKG40LM",
"ZNQBKG41LM",
].includes(meta.mapped.model)) {
await entity.read("manuSpecificLumi", [0x0203], manufacturerOptions.lumi);
}
else {
throw new Error("Not supported");
}
},
},
lumi_flip_indicator_light: {
key: ["flip_indicator_light"],
convertSet: async (entity, key, value, meta) => {
const lookup = { OFF: 0, ON: 1 };
await entity.write("manuSpecificLumi", { 240: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { flip_indicator_light: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x00f0], manufacturerOptions.lumi);
},
},
lumi_power_outage_count: {
key: ["power_outage_count"],
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0002], manufacturerOptions.lumi);
},
},
// lumi class specific
lumi_feeder: {
key: ["feed", "schedule", "led_indicator", "child_lock", "mode", "serving_size", "portion_weight"],
convertSet: async (entity, key, value, meta) => {
const sendAttr = async (attrCode, value, length) => {
// @ts-expect-error ignore
entity.sendSeq = ((entity.sendSeq || 0) + 1) % 256;
// @ts-expect-error ignore
const val = node_buffer_1.Buffer.from([0x00, 0x02, entity.sendSeq, 0, 0, 0, 0, 0]);
// @ts-expect-error ignore
entity.sendSeq += 1;
val.writeInt32BE(attrCode, 3);
val.writeUInt8(length, 7);
let v = node_buffer_1.Buffer.alloc(length);
switch (length) {
case 1:
v.writeUInt8(value);
break;
case 2:
v.writeUInt16BE(value);
break;
case 4:
v.writeUInt32BE(value);
break;
default:
// @ts-expect-error ignore
v = value;
}
await entity.write("manuSpecificLumi", { 65521: { value: node_buffer_1.Buffer.concat([val, v]), type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
};
switch (key) {
case "feed":
await sendAttr(0x04150055, 1, 1);
break;
case "schedule": {
const schedule = [];
// @ts-expect-error ignore
// biome-ignore lint/complexity/noForEach: ignored using `--suppress`
value.forEach((item) => {
const schedItem = node_buffer_1.Buffer.from([(0, utils_1.getKey)(feederDaysLookup, item.days, 0x7f), item.hour, item.minute, item.size, 0]);
schedule.push(schedItem.toString("hex"));
});
const val = node_buffer_1.Buffer.concat([node_buffer_1.Buffer.from(schedule.join(",")), node_buffer_1.Buffer.from([0])]);
// @ts-expect-error ignore
await sendAttr(0x080008c8, val, val.length);
break;
}
case "led_indicator":
await sendAttr(0x04170055, (0, utils_1.getFromLookup)(value, { ON: 1, OFF: 0 }), 1);
break;
case "child_lock":
await sendAttr(0x04160055, (0, utils_1.getFromLookup)(value, { UNLOCK: 0, LOCK: 1 }), 1);
break;
case "mode":
await sendAttr(0x04180055, (0, utils_1.getFromLookup)(value, { manual: 0, schedule: 1 }), 1);
break;
case "serving_size":
// @ts-expect-error ignore
await sendAttr(0x0e5c0055, value, 4);
break;
case "portion_weight":
// @ts-expect-error ignore
await sendAttr(0x0e5f0055, value, 4);
break;
default: // Unknown key
logger_1.logger.warning(`Unhandled key ${key}`, "zhc:lumi:feeder");
}
return { state: { [key]: value } };
},
},
lumi_detection_distance: {
key: ["detection_distance"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertString)(value, "detection_distance");
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = value.toLowerCase();
const lookup = { "10mm": 1, "20mm": 2, "30mm": 3 };
await entity.write("manuSpecificLumi", { 268: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, { manufacturerCode: exports.manufacturerCode });
return { state: { detection_distance: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x010c], { manufacturerCode: exports.manufacturerCode });
},
},
lumi_trv: {
key: [
"system_mode",
"preset",
"window_detection",
"valve_detection",
"child_lock",
"away_preset_temperature",
"calibrate",
"sensor",
"external_temperature_input",
"identify",
"schedule",
"schedule_settings",
],
convertSet: async (entity, key, value, meta) => {
const lumiHeader = (counter, params, action) => {
const header = [0xaa, 0x71, params.length + 3, 0x44, counter];
const integrity = 512 - header.reduce((sum, elem) => sum + elem, 0);
return [...header, integrity, action, 0x41, params.length];
};
const sensor = node_buffer_1.Buffer.from("00158d00019d1b98", "hex");
switch (key) {
case "system_mode":
await entity.write("manuSpecificLumi", { 625: { value: (0, utils_1.getFromLookup)(value, { off: 0, heat: 1 }), type: 0x20 } }, { manufacturerCode: exports.manufacturerCode });
break;
case "preset":
await entity.write("manuSpecificLumi", { 626: { value: (0, utils_1.getFromLookup)(value, { manual: 0, auto: 1, away: 2 }), type: 0x20 } }, { manufacturerCode: exports.manufacturerCode });
break;
case "window_detection":
await entity.write("manuSpecificLumi", {
627: { value: (0, utils_1.getFromLookup)(value, { false: 0, true: 1 }, undefined, true), type: 0x20 },
}, { manufacturerCode: exports.manufacturerCode });
break;
case "valve_detection":
await entity.write("manuSpecificLumi", {
628: { value: (0, utils_1.getFromLookup)(value, { false: 0, true: 1 }, undefined, true), type: 0x20 },
}, { manufacturerCode: exports.manufacturerCode });
break;
case "child_lock":
await entity.write("manuSpecificLumi", {
631: { value: (0, utils_1.getFromLookup)(value, { UNLOCK: 0, LOCK: 1 }), type: 0x20 },
}, { manufacturerCode: exports.manufacturerCode });
break;
case "away_preset_temperature":
await entity.write("manuSpecificLumi", {
633: { value: Math.round((0, utils_1.toNumber)(value, "away_preset_temperature") * 100), type: 0x23 },
}, { manufacturerCode: exports.manufacturerCode });
break;
case "sensor": {
(0, utils_1.assertEndpoint)(entity);
const device = node_buffer_1.Buffer.from(entity.deviceIeeeAddress.substring(2), "hex");
const timestamp = node_buffer_1.Buffer.alloc(4);
timestamp.writeUInt32BE(Date.now() / 1000);
if (value === "external") {
const params1 = [
...timestamp,
0x3d,
0x04,
...device,
...sensor,
0x00,
0x01,
0x00,
0x55,
0x13,
0x0a,
0x02,
0x00,
0x00,
0x64,
0x04,
0xce,
0xc2,
0xb6,
0xc8,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x3d,
0x64,
0x65,
];
const params2 = [
...timestamp,
0x3d,
0x05,
...device,
...sensor,
0x08,
0x00,
0x07,
0xfd,
0x16,
0x0a,
0x02,
0x0a,
0xc9,
0xe8,
0xb1,
0xb8,
0xd4,
0xda,
0xcf,
0xdf,
0xc0,
0xeb,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x3d,
0x04,
0x65,
];
const val1 = [...lumiHeader(0x12, params1, 0x02), ...params1];
const val2 = [...lumiHeader(0x13, params2, 0x02), ...params2];
await entity.write("manuSpecificLumi", { 65522: { value: val1, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
await entity.write("manuSpecificLumi", { 65522: { value: val2, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
}
else if (value === "internal") {
const params1 = [...timestamp, 0x3d, 0x05, ...device, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
const params2 = [...timestamp, 0x3d, 0x04, ...device, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
const val1 = [...lumiHeader(0x12, params1, 0x04), ...params1];
const val2 = [...lumiHeader(0x13, params2, 0x04), ...params2];
await entity.write("manuSpecificLumi", { 65522: { value: val1, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
await entity.write("manuSpecificLumi", { 65522: { value: val2, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
await entity.read("hvacThermostat", ["localTemp"]);
}
break;
}
case "external_temperature_input":
if (meta.state.sensor === "external") {
const temperatureBuf = node_buffer_1.Buffer.alloc(4);
const number = (0, utils_1.toNumber)(value);
temperatureBuf.writeFloatBE(Math.round(number * 100));
const params = [...sensor, 0x00, 0x01, 0x00, 0x55, ...temperatureBuf];
const data = [...lumiHeader(0x12, params, 0x05), ...params];
await entity.write("manuSpecificLumi", { 65522: { value: data, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
}
break;
case "calibrate":
await entity.write("manuSpecificLumi", { 624: { value: 1, type: 0x20 } }, { manufacturerCode: exports.manufacturerCode });
break;
case "identify":
await entity.command("genIdentify", "identify", { identifytime: 5 }, {});
break;
case "schedule":
await entity.write("manuSpecificLumi", {
637: { value: (0, utils_1.getFromLookup)(value, { false: 0, true: 1 }, undefined, true), type: 0x20 },
}, { manufacturerCode: exports.manufacturerCode });
break;
case "schedule_settings": {
// @ts-expect-error ignore
const schedule = exports.trv.parseSchedule(value);
exports.trv.validateSchedule(schedule);
const buffer = exports.trv.encodeSchedule(schedule);
await entity.write("manuSpecificLumi", { 630: { value: buffer, type: 0x41 } }, { manufacturerCode: exports.manufacturerCode });
break;
}
default: // Unknown key
logger_1.logger.debug(`Unhandled key ${key}`, "zhc:lumi:trv");
}
},
convertGet: async (entity, key, meta) => {
const dict = {
system_mode: 0x0271,
preset: 0x0272,
window_detection: 0x0273,
valve_detection: 0x0274,
child_lock: 0x0277,
away_preset_temperature: 0x0279,
calibrated: 0x027b,
sensor: 0x027e,
schedule: 0x027d,
schedule_settings: 0x0276,
};
await entity.read("manuSpecificLumi", [(0, utils_1.getFromLookup)(key, dict)], { manufacturerCode: exports.manufacturerCode });
},
},
lumi_presence_region_upsert: {
key: ["region_upsert"],
convertSet: async (entity, key, value, meta) => {
const commandWrapper = exports.presence.parseAqaraFp1RegionUpsertInput(value);
if (!commandWrapper.isSuccess) {
logger_1.logger.warning(
// @ts-expect-error untyped
`Encountered an error (${commandWrapper.error.reason}) while parsing configuration commands (input: ${JSON.stringify(value)})`, NS);
return;
}
const command = commandWrapper.payload.command;
logger_1.logger.debug(`Trying to create region ${command.region_id}`, NS);
const sortedZonesAccumulator = {};
const sortedZonesWithSets = command.zones.reduce((accumulator, zone) => {
if (!accumulator[zone.y]) {
accumulator[zone.y] = new Set();
}
accumulator[zone.y].add(zone.x);
return accumulator;
}, sortedZonesAccumulator);
const sortedZones = Object.entries(sortedZonesWithSets).reduce((acc, [key, value]) => {
const numKey = Number.parseInt(key, 10); // Convert string key back to number
acc[numKey] = Array.from(value);
return acc;
}, {});
const deviceConfig = new Uint8Array(7);
// Command parameters
deviceConfig[0] = exports.presence.constants.region_config_cmds.create;
deviceConfig[1] = command.region_id;
deviceConfig[6] = exports.presence.constants.region_config_cmd_suffix_upsert;
// Zones definition
deviceConfig[2] |= exports.presence.encodeXCellsDefinition(sortedZones["1"]);
deviceConfig[2] |= exports.presence.encodeXCellsDefinition(sortedZones["2"]) << 4;
deviceConfig[3] |= exports.presence.encodeXCellsDefinition(sortedZones["3"]);
deviceConfig[3] |= exports.presence.encodeXCellsDefinition(sortedZones["4"]) << 4;
deviceConfig[4] |= exports.presence.encodeXCellsDefinition(sortedZones["5"]);
deviceConfig[4] |= exports.presence.encodeXCellsDefinition(sortedZones["6"]) << 4;
deviceConfig[5] |= exports.presence.encodeXCellsDefinition(sortedZones["7"]);
logger_1.logger.info(`Create region ${command.region_id} ${(0, utils_1.printNumbersAsHexSequence)([...deviceConfig], 2)}`, NS);
const payload = {
[exports.presence.constants.region_config_write_attribute]: {
value: deviceConfig,
type: exports.presence.constants.region_config_write_attribute_type,
},
};
await entity.write("manuSpecificLumi", payload, { manufacturerCode: exports.manufacturerCode });
},
},
lumi_presence_region_delete: {
key: ["region_delete"],
convertSet: async (entity, key, value, meta) => {
const commandWrapper = exports.presence.parseAqaraFp1RegionDeleteInput(value);
if (!commandWrapper.isSuccess) {
logger_1.logger.warning(
// @ts-expect-error ignore
`Encountered an error (${commandWrapper.error.reason}) while parsing configuration commands (input: ${JSON.stringify(value)})`, NS);
return;
}
const command = commandWrapper.payload.command;
logger_1.logger.debug(`trying to delete region ${command.region_id}`, NS);
const deviceConfig = new Uint8Array(7);
// Command parameters
deviceConfig[0] = exports.presence.constants.region_config_cmds.delete;
deviceConfig[1] = command.region_id;
deviceConfig[6] = exports.presence.constants.region_config_cmd_suffix_delete;
// Zones definition
deviceConfig[2] = 0;
deviceConfig[3] = 0;
deviceConfig[4] = 0;
deviceConfig[5] = 0;
logger_1.logger.info(`Delete region ${command.region_id} (${(0, utils_1.printNumbersAsHexSequence)([...deviceConfig], 2)})`, NS);
const payload = {
[exports.presence.constants.region_config_write_attribute]: {
value: deviceConfig,
type: exports.presence.constants.region_config_write_attribute_type,
},
};
await entity.write("manuSpecificLumi", payload, { manufacturerCode: exports.manufacturerCode });
},
},
lumi_cube_operation_mode: {
key: ["operation_mode"],
convertSet: (entity, key, value, meta) => {
const lookup = { action_mode: 0, scene_mode: 1 };
/**
* schedule the callback to run when the configuration window comes
*/
const callback = async () => {
await entity.write("manuSpecificLumi", { 328: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, { manufacturerCode: exports.manufacturerCode, disableDefaultResponse: true });
logger_1.logger.info("operation_mode switch success!", "zhc:lumi:cube");
};
globalStore.putValue(meta.device, "opModeSwitchTask", { callback, newMode: value });
logger_1.logger.info("Now give your cube a forceful throw motion (Careful not to drop it)!", "zhc:lumi:cube");
},
},
lumi_switch_operation_mode_basic: {
key: ["operation_mode"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertEndpoint)(entity);
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
let targetValue = (0, utils_1.isObject)(value) && value.state != null ? value.state : value;
// 1/2 gang switches using genBasic on endpoint 1.
// biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
let attrId;
let attrValue;
if (meta.mapped.meta?.multiEndpoint) {
attrId = { left: 0xff22, right: 0xff23 }[meta.endpoint_name];
// Allow usage of control_relay for 2 gang switches by mapping it to the default side.
if (targetValue === "control_relay") {
targetValue = `control_${meta.endpoint_name}_relay`;
}
attrValue = (0, utils_1.getFromLookup)(targetValue, { control_left_relay: 0x12, control_right_relay: 0x22, decoupled: 0xfe });
if (attrId == null) {
throw new Error(`Unsupported endpoint ${meta.endpoint_name} for changing operation_mode.`);
}
}
else {
attrId = 0xff22;
attrValue = (0, utils_1.getFromLookup)(targetValue, { control_relay: 0x12, decoupled: 0xfe });
}
if (attrValue == null) {
throw new Error("Invalid operation_mode value");
}
const endpoint = entity.getDevice().getEndpoint(1);
const payload = {};
payload[attrId] = { value: attrValue, type: 0x20 };
await endpoint.write("genBasic", payload, manufacturerOptions.lumi);
return { state: { operation_mode: targetValue } };
},
convertGet: async (entity, key, meta) => {
// biome-ignore lint/suspicious/noImplicitAnyLet: ignored using `--suppress`
let attrId;
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if (meta.mapped.meta?.multiEndpoint) {
attrId = { left: 0xff22, right: 0xff23 }[meta.endpoint_name];
if (attrId == null) {
throw new Error(`Unsupported endpoint ${meta.endpoint_name} for getting operation_mode.`);
}
}
else {
attrId = 0xff22;
}
await entity.read("genBasic", [attrId], manufacturerOptions.lumi);
},
},
lumi_switch_operation_mode_opple: {
key: ["operation_mode"],
convertSet: async (entity, key, value, meta) => {
// Support existing syntax of a nested object just for the state field. Though it's quite silly IMO.
const targetValue = (0, utils_1.isObject)(value) && value.state != null ? value.state : value;
// Switches using manuSpecificLumi 0x0200 on the same endpoints as the onOff clusters.
const lookupState = { control_relay: 0x01, decoupled: 0x00 };
await entity.write("manuSpecificLumi", { 512: { value: (0, utils_1.getFromLookup)(targetValue, lookupState), type: 0x20 } }, manufacturerOptions.lumi);
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0200], manufacturerOptions.lumi);
},
},
lumi_detection_interval: {
key: ["detection_interval"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertNumber)(value, key);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value *= 1;
await entity.write("manuSpecificLumi", { 258: { value: [value], type: 0x20 } }, manufacturerOptions.lumi);
return { state: { detection_interval: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0102], manufacturerOptions.lumi);
},
},
lumi_overload_protection: {
key: ["overload_protection"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertNumber)(value, key);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value *= 1;
await entity.write("manuSpecificLumi", { 523: { value: [value], type: 0x39 } }, manufacturerOptions.lumi);
return { state: { overload_protection: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x020b], manufacturerOptions.lumi);
},
},
lumi_switch_mode_switch: {
key: ["mode_switch"],
convertSet: async (entity, key, value, meta) => {
const lookup = { anti_flicker_mode: 4, quick_mode: 1 };
await entity.write("manuSpecificLumi", { 4: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x21 } }, manufacturerOptions.lumi);
return { state: { mode_switch: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0004], manufacturerOptions.lumi);
},
},
lumi_button_switch_mode: {
key: ["button_switch_mode"],
convertSet: async (entity, key, value, meta) => {
const lookup = { relay: 0, relay_and_usb: 1 };
await entity.write("manuSpecificLumi", { 550: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { button_switch_mode: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0226], manufacturerOptions.lumi);
},
},
lumi_socket_button_lock: {
key: ["button_lock"],
convertSet: async (entity, key, value, meta) => {
const lookup = { ON: 0, OFF: 1 };
await entity.write("manuSpecificLumi", { 512: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { button_lock: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0200], manufacturerOptions.lumi);
},
},
lumi_dimmer_mode: {
key: ["dimmer_mode"],
convertSet: async (entity, key, value, meta) => {
const lookup = { rgbw: 3, dual_ct: 1 };
(0, utils_1.assertString)(value, key);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = value.toLowerCase();
// @ts-expect-error ignore
if (["rgbw"].includes(value)) {
await entity.write("manuSpecificLumi", { 1289: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x23 } }, manufacturerOptions.lumi);
await entity.write("manuSpecificLumi", { 1295: { value: 1, type: 0x23 } }, manufacturerOptions.lumi);
}
else {
await entity.write("manuSpecificLumi", { 1289: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x23 } }, manufacturerOptions.lumi);
// Turn on dimming channel 1 and channel 2
await entity.write("manuSpecificLumi", { 1295: { value: 3, type: 0x23 } }, manufacturerOptions.lumi);
}
return { state: { dimmer_mode: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0509], manufacturerOptions.lumi);
},
},
lumi_switch_do_not_disturb: {
key: ["do_not_disturb"],
convertSet: async (entity, key, value, meta) => {
await entity.write("manuSpecificLumi", { 515: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.lumi);
return { state: { do_not_disturb: value } };
},
},
lumi_switch_type: {
key: ["switch_type"],
convertSet: async (entity, key, value, meta) => {
const lookup = { toggle: 1, momentary: 2 };
(0, utils_1.assertString)(value, key);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = value.toLowerCase();
await entity.write("manuSpecificLumi", { 10: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { switch_type: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x000a], manufacturerOptions.lumi);
},
},
lumi_switch_power_outage_memory: {
key: ["power_outage_memory"],
convertSet: async (entity, key, value, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if ([
"SP-EUC01",
"ZNCZ04LM",
"ZNCZ15LM",
"QBCZ14LM",
"QBCZ15LM",
"SSM-U01",
"SSM-U02",
"DLKZMK11LM",
"DLKZMK12LM",
"WS-EUK01",
"WS-EUK02",
"WS-EUK03",
"WS-EUK04",
"QBKG17LM",
"QBKG18LM",
"QBKG19LM",
"QBKG20LM",
"QBKG25LM",
"QBKG26LM",
"QBKG27LM",
"QBKG28LM",
"QBKG29LM",
"QBKG30LM",
"QBKG31LM",
"QBKG32LM",
"QBKG33LM",
"QBKG34LM",
"QBKG38LM",
"QBKG39LM",
"QBKG40LM",
"QBKG41LM",
"ZNDDMK11LM",
"ZNLDP13LM",
"ZNQBKG31LM",
"WS-USC02",
"WS-USC03",
"WS-USC04",
"ZNQBKG24LM",
"ZNQBKG25LM",
"ZNQBKG26LM",
"JWDL001A",
"SSWQD02LM",
"SSWQD03LM",
"XDD11LM",
"XDD12LM",
"XDD13LM",
"ZNLDP12LM",
"ZNLDP13LM",
"ZNXDD01LM",
"WS-USC01",
].includes(meta.mapped.model)) {
await entity.write("manuSpecificLumi", { 513: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.lumi);
}
else if (["ZNCZ02LM", "QBCZ11LM", "LLKZMK11LM"].includes(meta.mapped.model)) {
const payload = value
? [
[0xaa, 0x80, 0x05, 0xd1, 0x47, 0x07, 0x01, 0x10, 0x01],
[0xaa, 0x80, 0x03, 0xd3, 0x07, 0x08, 0x01],
]
: [
[0xaa, 0x80, 0x05, 0xd1, 0x47, 0x09, 0x01, 0x10, 0x00],
[0xaa, 0x80, 0x03, 0xd3, 0x07, 0x0a, 0x01],
];
await entity.write("genBasic", { 65520: { value: payload[0], type: 0x41 } }, manufacturerOptions.lumi);
await entity.write("genBasic", { 65520: { value: payload[1], type: 0x41 } }, manufacturerOptions.lumi);
}
else if (["ZNCZ11LM", "ZNCZ12LM"].includes(meta.mapped.model)) {
const payload = value
? [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x00, 0x01, 0x10, 0x01]
: [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x01, 0x01, 0x10, 0x00];
await entity.write("genBasic", { 65520: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
}
else if (["ZNQBKG38LM", "ZNQBKG39LM", "ZNQBKG40LM", "ZNQBKG41LM"].includes(meta.mapped.model)) {
// Support existing syntax of a nested object just for the state field. Though it's quite silly IMO.
const targetValue = (0, utils_1.isObject)(value) && value.state != null ? value.state : value;
const lookupState = { on: 0x01, electric_appliances_on: 0x00, electric_appliances_off: 0x02, inverted: 0x03 };
await entity.write("manuSpecificLumi", { 1303: { value: (0, utils_1.getFromLookup)(targetValue, lookupState), type: 0x20 } }, manufacturerOptions.lumi);
}
else {
throw new Error("Not supported");
}
return { state: { power_outage_memory: value } };
},
convertGet: async (entity, key, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if ([
"SP-EUC01",
"ZNCZ04LM",
"ZNCZ15LM",
"QBCZ14LM",
"QBCZ15LM",
"SSM-U01",
"SSM-U02",
"DLKZMK11LM",
"DLKZMK12LM",
"WS-EUK01",
"WS-EUK02",
"WS-EUK03",
"WS-EUK04",
"QBKG17LM",
"QBKG18LM",
"QBKG19LM",
"QBKG20LM",
"QBKG25LM",
"QBKG26LM",
"QBKG27LM",
"QBKG28LM",
"QBKG29LM",
"QBKG30LM",
"QBKG31LM",
"QBKG32LM",
"QBKG33LM",
"QBKG34LM",
"QBKG38LM",
"QBKG39LM",
"QBKG40LM",
"QBKG41LM",
"ZNDDMK11LM",
"ZNLDP13LM",
"ZNQBKG31LM",
"WS-USC02",
"WS-USC03",
"WS-USC04",
"ZNQBKG24LM",
"ZNQBKG25LM",
"ZNQBKG26LM",
"JWDL001A",
"SSWQD02LM",
"SSWQD03LM",
"XDD11LM",
"XDD12LM",
"XDD13LM",
"ZNLDP12LM",
"ZNLDP13LM",
"ZNXDD01LM",
"WS-USC01",
].includes(meta.mapped.model)) {
await entity.read("manuSpecificLumi", [0x0201]);
}
else if (["ZNCZ02LM", "QBCZ11LM", "ZNCZ11LM", "ZNCZ12LM"].includes(meta.mapped.model)) {
await entity.read("manuSpecificLumi", [0xfff0]);
}
else if (["ZNQBKG38LM", "ZNQBKG39LM", "ZNQBKG40LM", "ZNQBKG41LM"].includes(meta.mapped.model)) {
await entity.read("manuSpecificLumi", [0x0517]);
}
else {
throw new Error("Not supported");
}
},
},
lumi_light_power_outage_memory: {
key: ["power_outage_memory"],
convertSet: async (entity, key, value, meta) => {
await entity.write("genBasic", { 65305: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.lumi);
return { state: { power_outage_memory: value } };
},
},
lumi_auto_off: {
key: ["auto_off"],
convertSet: async (entity, key, value, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if (["ZNCZ04LM", "ZNCZ12LM", "SP-EUC01"].includes(meta.mapped.model)) {
await entity.write("manuSpecificLumi", { 514: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.lumi);
}
else if (["ZNCZ11LM"].includes(meta.mapped.model)) {
const payload = value
? [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x00, 0x02, 0x10, 0x01]
: [0xaa, 0x80, 0x05, 0xd1, 0x47, 0x01, 0x02, 0x10, 0x00];
await entity.write("genBasic", { 65520: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
}
else {
throw new Error("Not supported");
}
return { state: { auto_off: value } };
},
convertGet: async (entity, key, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if (["ZNCZ04LM", "ZNCZ12LM", "SP-EUC01"].includes(meta.mapped.model)) {
await entity.read("manuSpecificLumi", [0x0202], manufacturerOptions.lumi);
}
else {
throw new Error("Not supported");
}
},
},
lumi_detection_period: {
key: ["detection_period"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertNumber)(value, key);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value *= 1;
await entity.write("manuSpecificLumi", { 0: { value: [value], type: 0x21 } }, manufacturerOptions.lumi);
return { state: { detection_period: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0000], manufacturerOptions.lumi);
},
},
lumi_motion_sensitivity: {
key: ["motion_sensitivity"],
convertSet: async (entity, key, value, meta) => {
const lookup = { low: 1, medium: 2, high: 3 };
(0, utils_1.assertString)(value, key);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = value.toLowerCase();
await entity.write("manuSpecificLumi", { 268: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { motion_sensitivity: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x010c], manufacturerOptions.lumi);
},
},
lumi_presence: {
key: ["presence"],
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0142], manufacturerOptions.lumi);
},
},
lumi_monitoring_mode: {
key: ["monitoring_mode"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertString)(value, key);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = value.toLowerCase();
const lookup = { undirected: 0, left_right: 1 };
await entity.write("manuSpecificLumi", { 324: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { monitoring_mode: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0144], manufacturerOptions.lumi);
},
},
lumi_approach_distance: {
key: ["approach_distance"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertString)(value, key);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = value.toLowerCase();
const lookup = { far: 0, medium: 1, near: 2 };
await entity.write("manuSpecificLumi", { 326: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { approach_distance: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0146], manufacturerOptions.lumi);
},
},
lumi_reset_nopresence_status: {
key: ["reset_nopresence_status"],
convertSet: async (entity, key, value, meta) => {
await entity.write("manuSpecificLumi", { 343: { value: 1, type: 0x20 } }, manufacturerOptions.lumi);
},
},
lumi_switch_click_mode: {
key: ["click_mode"],
convertSet: async (entity, key, value, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if (["ZNQBKG38LM", "ZNQBKG39LM", "ZNQBKG40LM", "ZNQBKG41LM"].includes(meta.mapped.model)) {
await entity.write("manuSpecificLumi", { 646: { value: (0, utils_1.getFromLookup)(value, { fast: 0x1, multi: 0x02 }), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { click_mode: value } };
}
await entity.write("manuSpecificLumi", { 293: { value: (0, utils_1.getFromLookup)(value, { fast: 0x1, multi: 0x02 }), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { click_mode: value } };
},
convertGet: async (entity, key, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if (["ZNQBKG38LM", "ZNQBKG39LM", "ZNQBKG40LM", "ZNQBKG41LM"].includes(meta.mapped.model)) {
await entity.read("manuSpecificLumi", [0x0286], manufacturerOptions.lumi);
}
else {
await entity.read("manuSpecificLumi", [0x125], manufacturerOptions.lumi);
}
},
},
lumi_switch_lock_relay_opple: {
key: ["lock_relay"],
convertSet: async (entity, key, value, meta) => {
await entity.write("manuSpecificLumi", { 645: { value: value ? 1 : 0, type: 0x20 } }, manufacturerOptions.lumi);
return { state: { lock_relay: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0285], manufacturerOptions.lumi);
},
},
lumi_operation_mode_opple: {
key: ["operation_mode"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertString)(value);
// modes:
// 0 - 'command' mode. keys send commands. useful for binding
// 1 - 'event' mode. keys send events. useful for handling
const lookup = { command: 0, event: 1 };
const endpoint = meta.device.getEndpoint(1);
await endpoint.write("manuSpecificLumi", { mode: (0, utils_1.getFromLookup)(value.toLowerCase(), lookup) }, { manufacturerCode: manufacturerOptions.lumi.manufacturerCode });
return { state: { operation_mode: value.toLowerCase() } };
},
convertGet: async (entity, key, meta) => {
const endpoint = meta.device.getEndpoint(1);
await endpoint.read("manuSpecificLumi", ["mode"], { manufacturerCode: manufacturerOptions.lumi.manufacturerCode });
},
},
lumi_vibration_sensitivity: {
key: ["sensitivity"],
convertSet: async (entity, key, value, meta) => {
if ((0, utils_1.isString)(value)) {
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = (0, utils_1.getFromLookup)(value, { low: 0x15, medium: 0x0b, high: 0x01 });
}
(0, utils_1.assertNumber)(value);
const options = { ...manufacturerOptions.lumi, timeout: 35000 };
await entity.write("genBasic", { 65293: { value, type: 0x20 } }, options);
return { state: { sensitivity: value } };
},
},
lumi_interlock: {
key: ["interlock"],
convertSet: async (entity, key, value, meta) => {
await entity.write("genBinaryOutput", { 65286: { value: value ? 0x01 : 0x00, type: 0x10 } }, manufacturerOptions.lumi);
return { state: { interlock: value } };
},
},
lumi_curtain_options: {
key: ["options"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertObject)(value);
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
const opts = {
reverse_direction: false,
hand_open: true,
reset_limits: false,
...value,
};
// Legacy names
if (value.auto_close != null)
opts.hand_open = value.auto_close;
if (value.reset_move != null)
opts.reset_limits = value.reset_move;
if (meta.mapped.model === "ZNCLDJ12LM") {
await entity.write("genBasic", { 65320: { value: opts.reverse_direction, type: 0x10 } }, manufacturerOptions.lumi);
await entity.write("genBasic", { 65321: { value: !opts.hand_open, type: 0x10 } }, manufacturerOptions.lumi);
if (opts.reset_limits) {
await entity.write("genBasic", { 65319: { value: 0x00, type: 0x10 } }, manufacturerOptions.lumi);
}
}
else if (meta.mapped.model === "ZNCLDJ11LM") {
const payload = [
0x07,
0x00,
opts.reset_limits ? 0x01 : 0x02,
0x00,
opts.reverse_direction ? 0x01 : 0x00,
0x04,
!opts.hand_open ? 0x01 : 0x00,
0x12,
];
await entity.write("genBasic", { 1025: { value: payload, type: 0x42 } }, manufacturerOptions.lumi);
// hand_open requires a separate request with slightly different payload
payload[2] = 0x08;
await entity.write("genBasic", { 1025: { value: payload, type: 0x42 } }, manufacturerOptions.lumi);
}
else {
throw new Error(`lumi_curtain_options set called for not supported model: ${meta.mapped.model}`);
}
// Reset limits is an action, not a state.
// biome-ignore lint/performance/noDelete: ignored using `--suppress`
delete opts.reset_limits;
return { state: { options: opts } };
},
convertGet: async (entity, key, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if (meta.mapped.model === "ZNCLDJ11LM") {
await entity.read("genBasic", [0x0401], manufacturerOptions.lumi);
}
else {
throw new Error(`lumi_curtain_options get called for not supported model: ${meta.mapped.model}`);
}
},
},
lumi_curtain_position_state: {
key: ["state", "position"],
options: [exposes.options.invert_cover()],
convertSet: async (entity, key, value, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if (key === "state" && typeof value === "string" && value.toLowerCase() === "stop") {
if (["ZNJLBL01LM", "ZNCLDJ14LM"].includes(meta.mapped.model)) {
const payload = { presentValue: 2 };
await entity.write("genMultistateOutput", payload);
}
else {
await entity.command("closuresWindowCovering", "stop", {}, (0, utils_1.getOptions)(meta.mapped, entity));
}
if (!["ZNCLDJ11LM", "ZNCLBL01LM"].includes(meta.mapped.model)) {
// The code below is originally added for ZNCLDJ11LM (Koenkk/zigbee2mqtt#4585).
// However, in Koenkk/zigbee-herdsman-converters#4039 it was replaced by reading
// directly from currentPositionLiftPercentage, so that device is excluded.
// For ZNJLBL01LM, in Koenkk/zigbee-herdsman-converters#4163 the position is read
// through onEvent each time the motor stops, so it becomes redundant, and the
// device is excluded.
// The code is left here to avoid breaking compatibility, ideally all devices using
// this converter should be tested so the code can be adjusted/deleted.
// Lumi curtain does not send position update on stop, request this.
await entity.read("genAnalogOutput", [0x0055]);
}
}
else {
const lookup = { open: 100, close: 0, on: 100, off: 0 };
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = typeof value === "string" ? value.toLowerCase() : value;
if ((0, utils_1.isString)(value)) {
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = (0, utils_1.getFromLookup)(value, lookup);
}
(0, utils_1.assertNumber)(value);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = meta.options.invert_cover ? 100 - value : value;
if (["ZNCLBL01LM"].includes(meta.mapped.model)) {
await entity.command("closuresWindowCovering", "goToLiftPercentage", { percentageliftvalue: value }, (0, utils_1.getOptions)(meta.mapped, entity));
}
else {
const payload = { presentValue: value };
await entity.write("genAnalogOutput", payload);
}
return { state: { position: value } };
}
},
convertGet: async (entity, key, meta) => {
if (!Array.isArray(meta.mapped) && "ZNJLBL01LM" === meta.mapped.model) {
// https://github.com/Koenkk/zigbee2mqtt/issues/23056
logger_1.logger.debug(`Not reading position of ZNJLBL01LM since device doesn't support it`, NS);
}
else if (!Array.isArray(meta.mapped) && ["ZNCLBL01LM"].includes(meta.mapped.model)) {
await entity.read("closuresWindowCovering", ["currentPositionLiftPercentage"]);
}
else {
await entity.read("genAnalogOutput", [0x0055]);
}
},
},
lumi_curtain_battery_voltage: {
key: ["voltage"],
convertGet: async (entity, key, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
switch (meta.mapped.model) {
case "ZNCLBL01LM":
await entity.read("manuSpecificLumi", [0x040b], manufacturerOptions.lumi);
break;
default:
throw new Error(`lumi_curtain_battery_voltage - unsupported model: ${meta.mapped.model}`);
}
},
},
lumi_curtain_charging_status: {
key: ["charging_status"],
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0409], manufacturerOptions.lumi);
},
},
lumi_curtain_battery: {
key: ["battery"],
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x040a], manufacturerOptions.lumi);
},
},
lumi_trigger_indicator: {
key: ["trigger_indicator"],
convertSet: async (entity, key, value, meta) => {
await entity.write("manuSpecificLumi", { 338: { value: value ? 1 : 0, type: 0x20 } }, manufacturerOptions.lumi);
return { state: { trigger_indicator: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0152], manufacturerOptions.lumi);
},
},
lumi_curtain_hooks_lock: {
key: ["hooks_lock"],
convertSet: async (entity, key, value, meta) => {
const lookup = { UNLOCK: 0, LOCK: 1 };
await entity.write("manuSpecificLumi", { 1063: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { [key]: value } };
},
},
lumi_curtain_hooks_state: {
key: ["hooks_state"],
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0428], manufacturerOptions.lumi);
},
},
lumi_curtain_hand_open: {
key: ["hand_open"],
convertSet: async (entity, key, value, meta) => {
await entity.write("manuSpecificLumi", { curtainHandOpen: !value }, manufacturerOptions.lumi);
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", ["curtainHandOpen"], manufacturerOptions.lumi);
},
},
lumi_curtain_reverse: {
key: ["reverse_direction"],
convertSet: async (entity, key, value, meta) => {
await entity.write("manuSpecificLumi", { curtainReverse: value }, manufacturerOptions.lumi);
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", ["curtainReverse"], manufacturerOptions.lumi);
},
},
lumi_curtain_limits_calibration: {
key: ["limits_calibration"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertString)(value);
const normalizedValue = value.toLowerCase();
switch (normalizedValue) {
case "start":
await entity.write("manuSpecificLumi", { 1031: { value: 0x01, type: 0x20 } }, manufacturerOptions.lumi);
break;
case "end":
await entity.write("manuSpecificLumi", { 1031: { value: 0x02, type: 0x20 } }, manufacturerOptions.lumi);
break;
case "reset":
await entity.write("manuSpecificLumi", { 1031: { value: 0x00, type: 0x20 } }, manufacturerOptions.lumi);
// also? await entity.write('manuSpecificLumi', {0x0402: {value: 0x00, type: 0x10}}, manufacturerOptions.lumi);
break;
}
},
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
lumi_curtain_limits_calibration_ZNCLDJ14LM: {
key: ["limits_calibration"],
options: [
e.enum("limits_calibration", ea.ALL, ["calibrated", "recalibrate", "open", "close"]).withDescription("Recalibrate the position limits"),
],
convertSet: async (entity, key, value, meta) => {
switch (value) {
case "recalibrate":
await entity.write("manuSpecificLumi", { curtainCalibrated: false }, manufacturerOptions.lumi);
break;
case "open":
await entity.write("genMultistateOutput", { presentValue: 1 }, manufacturerOptions.lumi);
break;
case "close":
await entity.write("genMultistateOutput", { presentValue: 0 }, manufacturerOptions.lumi);
break;
}
},
},
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
lumi_curtain_automatic_calibration_ZNCLDJ01LM: {
key: ["automatic_calibration"],
convertSet: async (entity, key, value, meta) => {
// Check if the curtain is already calibrated
const checkIfCalibrated = async () => {
const result = await entity.read("manuSpecificLumi", ["curtainCalibrated"]);
return result ? result.curtainCalibrated : false;
};
if (await checkIfCalibrated()) {
logger_1.logger.info("End positions already calibrated. Reset the calibration before proceeding.", NS);
return;
}
// Reset Calibration
await entity.write("manuSpecificLumi", { 1031: { value: 0x00, type: 0x20 } }, manufacturerOptions.lumi);
logger_1.logger.info("Starting the calibration process...", NS);
// Wait for 3 seconds
await (0, utils_1.sleep)(3000);
// Move the curtain to one direction
await entity.command("closuresWindowCovering", "goToLiftPercentage", { percentageliftvalue: 100 }, (0, utils_1.getOptions)(meta.mapped, entity));
logger_1.logger.info("Moving curtain and waiting to reach the end position.", NS);
// Wait until the curtain gets into a moving state, then wait until it gets blocked or stopped
const waitForStateTransition = async (initialStates, desiredStates) => {
return await new Promise((resolve) => {
const checkState = async () => {
const result = await entity.read("manuSpecificLumi", [0x0421]);
const state = result ? result[0x0421] : null;
if (!initialStates.includes(state)) {
const checkDesiredState = async () => {
const result = await entity.read("manuSpecificLumi", [0x0421]);
const state = result ? result[0x0421] : null;
if (desiredStates.includes(state)) {
resolve();
}
else {
setTimeout(checkDesiredState, 500);
}
};
setTimeout(checkDesiredState, 500);
}
else {
setTimeout(checkState, 500);
}
};
void checkState();
});
};
await waitForStateTransition([2, 3], [2, 3]);
// Wait for 1 second
await (0, utils_1.sleep)(1000);
// Set First Calibration Position
await entity.write("manuSpecificLumi", { 1031: { value: 0x01, type: 0x20 } }, manufacturerOptions.lumi);
logger_1.logger.info("End position 1 has been set.", NS);
// Wait for 3 seconds
await (0, utils_1.sleep)(3000);
// Move the curtain in the opposite direction
await entity.command("closuresWindowCovering", "goToLiftPercentage", { percentageliftvalue: 0 }, (0, utils_1.getOptions)(meta.mapped, entity));
logger_1.logger.info("Moving curtain in the opposite direction and waiting to reach the end position.", NS);
// Wait until the curtain gets into a moving state, then wait until it gets blocked or stopped
await waitForStateTransition([2, 3], [2, 3]);
// Wait for 1 second
await (0, utils_1.sleep)(1000);
// Set Second Calibration Position
await entity.write("manuSpecificLumi", { 1031: { value: 0x02, type: 0x20 } }, manufacturerOptions.lumi);
logger_1.logger.info("End position 2 has been set.", NS);
logger_1.logger.info("Calibration process completed.", NS);
},
},
lumi_buzzer: {
key: ["buzzer"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertString)(value, key);
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
const attribute = ["JY-GZ-01AQ"].includes(meta.mapped.model) ? 0x013e : 0x013f;
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = value.toLowerCase() === "alarm" ? 15361 : 15360;
await entity.write("manuSpecificLumi", { [`${attribute}`]: { value: [`${value}`], type: 0x23 } }, manufacturerOptions.lumi);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = value === 15361 ? 0 : 1;
await entity.write("manuSpecificLumi", { 294: { value: [`${value}`], type: 0x20 } }, manufacturerOptions.lumi);
},
},
lumi_buzzer_manual: {
key: ["buzzer_manual_alarm", "buzzer_manual_mute"],
convertGet: async (entity, key, meta) => {
if (key === "buzzer_manual_mute") {
await entity.read("manuSpecificLumi", [0x0126], manufacturerOptions.lumi);
}
else if (key === "buzzer_manual_alarm") {
await entity.read("manuSpecificLumi", [0x013d], manufacturerOptions.lumi);
}
},
},
lumi_heartbeat_indicator: {
key: ["heartbeat_indicator"],
convertSet: async (entity, key, value, meta) => {
await entity.write("manuSpecificLumi", { 316: { value: value ? 1 : 0, type: 0x20 } }, manufacturerOptions.lumi);
return { state: { heartbeat_indicator: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x013c], manufacturerOptions.lumi);
},
},
lumi_selftest: {
key: ["selftest"],
convertSet: async (entity, key, value, meta) => {
if (Array.isArray(meta.mapped))
throw new Error("Not supported for groups");
if (["JTYJ-GD-01LM/BW", "JTQJ-BF-01LM/BW"].includes(meta.mapped.model)) {
// Timeout of 30 seconds + required (https://github.com/Koenkk/zigbee2mqtt/issues/2287)
const options = { ...manufacturerOptions.lumi, timeout: 35000 };
await entity.write("ssIasZone", { 65521: { value: 0x03010000, type: 0x23 } }, options);
}
else {
await entity.write("manuSpecificLumi", { 295: { value: true, type: 0x10 } }, manufacturerOptions.lumi);
}
},
},
lumi_linkage_alarm: {
key: ["linkage_alarm"],
convertSet: async (entity, key, value, meta) => {
await entity.write("manuSpecificLumi", { 331: { value: value ? 1 : 0, type: 0x20 } }, manufacturerOptions.lumi);
return { state: { linkage_alarm: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x014b], manufacturerOptions.lumi);
},
},
lumi_state: {
key: ["state"],
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x0139], manufacturerOptions.lumi);
},
},
lumi_alarm: {
key: ["gas", "smoke"],
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x013a], manufacturerOptions.lumi);
},
},
lumi_density: {
key: ["gas_density", "smoke_density", "smoke_density_dbm"],
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x013b], manufacturerOptions.lumi);
},
},
lumi_sensitivity: {
key: ["sensitivity"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertString)(value, key);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = value.toLowerCase();
const lookup = { low: 0x04010000, medium: 0x04020000, high: 0x04030000 };
// Timeout of 30 seconds + required (https://github.com/Koenkk/zigbee2mqtt/issues/2287)
const options = { ...manufacturerOptions.lumi, timeout: 35000 };
await entity.write("ssIasZone", { 65521: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x23 } }, options);
return { state: { sensitivity: value } };
},
},
lumi_gas_sensitivity: {
key: ["gas_sensitivity"],
convertSet: async (entity, key, value, meta) => {
(0, utils_1.assertString)(value, key);
// biome-ignore lint/style/noParameterAssign: ignored using `--suppress`
value = value.toUpperCase();
const lookup = { "15%LEL": 1, "10%LEL": 2 };
await entity.write("manuSpecificLumi", { 268: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { gas_sensitivity: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read("manuSpecificLumi", [0x010c], manufacturerOptions.lumi);
},
},
// lumi device specific
// biome-ignore lint/style/useNamingConvention: ignored using `--suppress`
lumi_smart_panel_ZNCJMB14LM: {
key: [
"theme",
"standby_enabled",
"beep_volume",
"lcd_brightness",
"language",
"screen_saver_style",
"standby_time",
"font_size",
"lcd_auto_brightness_enabled",
"homepage",
"screen_saver_enabled",
"standby_lcd_brightness",
"available_switches",
"switch_1_text_icon",
"switch_2_text_icon",
"switch_3_text_icon",
],
convertSet: async (entity, key, value, meta) => {
if (key === "theme") {
const lookup = { classic: 0, concise: 1 };
await entity.write("manuSpecificLumi", { 533: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { theme: value } };
}
if (key === "standby_enabled") {
await entity.write("manuSpecificLumi", { 531: { value: value, type: 0x10 } }, manufacturerOptions.lumi);
return { state: { standby_enabled: value } };
}
if (key === "beep_volume") {
const lookup = { mute: 0, low: 1, medium: 2, high: 3 };
await entity.write("manuSpecificLumi", { 530: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { beep_volume: value } };
}
if (key === "lcd_brightness") {
await entity.write("manuSpecificLumi", { 529: { value: value, type: 0x20 } }, manufacturerOptions.lumi);
return { state: { lcd_brightness: value } };
}
if (key === "language") {
const lookup = { chinese: 0, english: 1 };
await entity.write("manuSpecificLumi", { 528: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { language: value } };
}
if (key === "screen_saver_style") {
const lookup = { classic: 1, "analog clock": 2 };
await entity.write("manuSpecificLumi", { 532: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { screen_saver_style: value } };
}
if (key === "standby_time") {
await entity.write("manuSpecificLumi", { 534: { value: value, type: 0x23 } }, manufacturerOptions.lumi);
return { state: { standby_time: value } };
}
if (key === "font_size") {
const lookup = { small: 3, medium: 4, large: 5 };
await entity.write("manuSpecificLumi", { 535: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { font_size: value } };
}
if (key === "lcd_auto_brightness_enabled") {
await entity.write("manuSpecificLumi", { 536: { value: value, type: 0x10 } }, manufacturerOptions.lumi);
return { state: { lcd_auto_brightness_enabled: value } };
}
if (key === "homepage") {
const lookup = { scene: 0, feel: 1, thermostat: 2, switch: 3 };
await entity.write("manuSpecificLumi", { 537: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { homepage: value } };
}
if (key === "screen_saver_enabled") {
await entity.write("manuSpecificLumi", { 545: { value: value, type: 0x10 } }, manufacturerOptions.lumi);
return { state: { screen_saver_enabled: value } };
}
if (key === "standby_lcd_brightness") {
await entity.write("manuSpecificLumi", { 546: { value: value, type: 0x20 } }, manufacturerOptions.lumi);
return { state: { standby_lcd_brightness: value } };
}
if (key === "available_switches") {
const lookup = { none: 0, "1": 1, "2": 2, "1 and 2": 3, "3": 4, "1 and 3": 5, "2 and 3": 6, all: 7 };
await entity.write("manuSpecificLumi", { 555: { value: (0, utils_1.getFromLookup)(value, lookup), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { available_switches: value } };
}
if (key === "switch_1_text_icon") {
const lookup = { "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "10": 10, "11": 11 };
const payload = [];
const statearr = {};
(0, utils_1.assertObject)(value);
if (value.switch_1_icon != null) {
payload.push((0, utils_1.getFromLookup)(value.switch_1_icon, lookup));
statearr.switch_1_icon = value.switch_1_icon;
}
else {
payload.push(1);
statearr.switch_1_icon = "1";
}
if (value.switch_1_text != null) {
payload.push(...value.switch_1_text.split("").map((c) => c.charCodeAt(0)));
statearr.switch_1_text = value.switch_1_text;
}
else {
// @ts-expect-error ignore
payload.push(..."".text.split("").map((c) => c.charCodeAt(0)));
statearr.switch_1_text = "";
}
await entity.write("manuSpecificLumi", { 547: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
return { state: statearr };
}
if (key === "switch_2_text_icon") {
const lookup = { "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "10": 10, "11": 11 };
const payload = [];
const statearr = {};
(0, utils_1.assertObject)(value);
if (value.switch_2_icon != null) {
payload.push((0, utils_1.getFromLookup)(value.switch_2_icon, lookup));
statearr.switch_2_icon = value.switch_2_icon;
}
else {
payload.push(1);
statearr.switch_2_icon = "1";
}
if (value.switch_2_text != null) {
payload.push(...value.switch_2_text.split("").map((c) => c.charCodeAt(0)));
statearr.switch_2_text = value.switch_2_text;
}
else {
// @ts-expect-error ignore
payload.push(..."".text.split("").map((c) => c.charCodeAt(0)));
statearr.switch_2_text = "";
}
await entity.write("manuSpecificLumi", { 548: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
return { state: statearr };
}
if (key === "switch_3_text_icon") {
const lookup = { "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "10": 10, "11": 11 };
const payload = [];
const statearr = {};
(0, utils_1.assertObject)(value);
if (value.switch_3_icon != null) {
payload.push((0, utils_1.getFromLookup)(value.switch_3_icon, lookup));
statearr.switch_3_icon = value.switch_3_icon;
}
else {
payload.push(1);
statearr.switch_3_icon = "1";
}
if (value.switch_3_text != null) {
payload.push(...value.switch_3_text.split("").map((c) => c.charCodeAt(0)));
statearr.switch_3_text = value.switch_3_text;
}
else {
// @ts-expect-error ignore
payload.push(..."".text.split("").map((c) => c.charCodeAt(0)));
statearr.switch_3_text = "";
}
await entity.write("manuSpecificLumi", { 549: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
return { state: statearr };
}
throw new Error(`Not supported: '${key}'`);
},
},
};
//# sourceMappingURL=lumi.js.map