zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
1,101 lines • 226 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 (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.legacyFromZigbee = 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 utils_1 = require("./utils");
const ota = __importStar(require("./ota"));
const fromZigbee_1 = __importDefault(require("../converters/fromZigbee"));
const globalStore = __importStar(require("./store"));
const modernExtend = __importStar(require("./modernExtend"));
const exposes = __importStar(require("./exposes"));
const logger_1 = require("./logger");
const NS = 'zhc:lumi';
const legacyFromZigbeeStore = {};
const e = exposes.presets;
const ea = exposes.access;
const buffer2DataObject = (meta, 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 && model.meta.battery && 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) && Array.isArray(value)) {
(0, utils_1.assertNumber)(value[1]);
let count = value[1];
// 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 = 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;
// DEPRECATED: remove illuminance_lux here.
payload.illuminance_lux = 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)) {
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
const temperature = 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)) {
// DEPRECATED: change illuminance_lux -> illuminance
(0, utils_1.assertNumber)(value);
payload.illuminance_lux = 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)) {
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
const humidity = 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 (!['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.100, 6: 0.105, 7: 0.110,
8: 0.115, 9: 0.120, 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':
{
// @ts-expect-error
const dataObject247 = (0, exports.buffer2DataObject)(meta, 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 (error) {
// 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.100, 6: 0.105, 7: 0.110,
8: 0.115, 9: 0.120, 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, { 0x01: 'control_relay', 0x00: '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
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 ? true : false;
}
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_lux = 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
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
if (['MCCGQ01LM'].includes(model.model)) {
// @ts-expect-error
payload.contact = value[0].elmVal === 0;
}
// @ts-expect-error
payload.voltage = value[1].elmVal;
if (model.meta && model.meta.battery && 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
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 (dataObject) => {
let result = {};
for (const [key, value] of Object.entries(dataObject)) {
switch (key) {
case '247':
{
// @ts-expect-error
const dataObject247 = (0, exports.buffer2DataObject)(meta, model, value);
const result247 = await numericAttributes2Lookup(dataObject247);
result = { ...result, ...result247 };
}
break;
case '65281':
{
// @ts-expect-error
const result65281 = await numericAttributes2Lookup(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,
},
region_config_regionId_min: 1,
region_config_regionId_max: 10,
region_config_zoneY_min: 1,
region_config_zoneY_max: 7,
region_config_zoneX_min: 1,
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,
},
},
};
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isAqaraFp1RegionId: (value) => {
return (typeof value === 'number' &&
value >= exports.presence.constants.region_config_regionId_min &&
value <= exports.presence.constants.region_config_regionId_max);
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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) {
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
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 = parseInt(parts[0]);
const minutes = 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' }