zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
1,133 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) {