UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

1,133 lines 251 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.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) {