UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

1,101 lines 226 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 (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' }