UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

589 lines 20.6 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; }; Object.defineProperty(exports, "__esModule", { value: true }); const buffalo_1 = require("../buffalo"); const logger_1 = require("../utils/logger"); const definition_1 = require("./definition"); const tstype_1 = require("./tstype"); const Utils = __importStar(require("./utils")); const NS = 'zh:controller:buffalozcl'; const aliases = { 'boolean': 'uint8', 'bitmap8': 'uint8', 'enum8': 'uint8', 'data8': 'uint8', 'data16': 'uint16', 'bitmap16': 'uint16', 'uint16': 'uint16', 'enum16': 'uint16', 'clusterId': 'uint16', 'attrId': 'uint16', 'data24': 'uint24', 'bitmap24': 'uint24', 'data32': 'uint32', 'bitmap32': 'uint32', 'uint32': 'uint32', 'tod': 'uint32', 'date': 'uint32', 'utc': 'uint32', 'bacOid': 'uint32', 'singlePrec': 'floatle', 'doublePrec': 'doublele', 'bitmap40': 'uint40', 'data40': 'uint40', 'bitmap48': 'uint48', 'data48': 'uint48', 'bitmap56': 'uint56', 'data56': 'uint56', 'bitmap64': 'uint64', 'data64': 'uint64', 'ieeeAddr': 'uint64', 'longOctetStr': 'longCharStr', 'secKey': 'buffer16', 'noData': 'EMPTY', 'unknown': 'EMPTY', 'bag': 'array', 'set': 'array', }; const extensionFieldSetsDateTypeLookup = { 6: ['uint8'], 8: ['uint8'], 258: ['uint8', 'unit8'], 768: ['uint16', 'uint16', 'uint16', 'uint8', 'uint8', 'uint8', 'uint16', 'uint16'], }; class BuffaloZcl extends buffalo_1.Buffalo { readUseDataType(options) { return this.read(options.dataType, options); } writeUseDataType(value, options) { this.write(options.dataType, value, options); } readArray() { const values = []; const elementType = definition_1.DataType[this.readUInt8()]; const numberOfElements = this.readUInt16(); for (let i = 0; i < numberOfElements; i++) { const value = this.read(elementType, {}); values.push(value); } return values; } writeArray(value) { const elTypeNumeric = typeof value.elementType === 'number' ? value.elementType : definition_1.DataType[value.elementType]; this.writeUInt8(elTypeNumeric); this.writeUInt16(value.elements.length); value.elements.forEach(element => { this.write(definition_1.DataType[elTypeNumeric], element, {}); }); } readStruct() { const values = []; const numberOfElements = this.readUInt16(); for (let i = 0; i < numberOfElements; i++) { const elementType = this.readUInt8(); const value = this.read(definition_1.DataType[elementType], {}); values.push({ elmType: elementType, elmVal: value }); } return values; } readOctetStr() { const length = this.readUInt8(); return this.readBuffer(length); } readCharStr(options) { const length = this.readUInt8(); if (options.attrId === 65281) { const value = {}; // Xiaomi struct parsing for (let i = 0; i < length; i++) { const index = this.readUInt8(); const dataType = definition_1.DataType[this.readUInt8()]; value[index] = this.read(dataType, {}); if (this.position === this.buffer.length) { break; } } return value; } else { return this.readUtf8String(length); } } writeCharStr(value) { if (typeof value === 'string') { this.writeUInt8(value.length); this.writeUtf8String(value); } else { this.writeBuffer(value, value.length); } } readLongCharStr() { const length = this.readUInt16(); return this.readUtf8String(length); } writeLongCharStr(value) { this.writeUInt16(value.length); this.writeUtf8String(value); } writeOctetStr(value) { this.writeUInt8(value.length); this.writeBuffer(value, value.length); } readExtensionFieldSets() { const value = []; while (this.isMore()) { const clstId = this.readUInt16(); const len = this.readUInt8(); const end = this.getPosition() + len; let index = 0; const extField = []; while (this.getPosition() < end) { extField.push(this.read(extensionFieldSetsDateTypeLookup[clstId][index], null)); index++; } value.push({ extField, clstId, len }); } return value; } writeExtensionFieldSets(values) { for (const value of values) { this.writeUInt16(value.clstId); this.writeUInt8(value.len); value.extField.forEach((entry, index) => { this.write(extensionFieldSetsDateTypeLookup[value.clstId][index], entry, null); }); } } writeListZoneInfo(values) { for (const value of values) { this.writeUInt8(value.zoneID); this.writeUInt16(value.zoneStatus); } } readListZoneInfo(options) { const value = []; for (let i = 0; i < options.length; i++) { value.push({ zoneID: this.readUInt8(), zoneStatus: this.readUInt16(), }); } return value; } readListThermoTransitions(options) { const heat = options.payload['mode'] & 1; const cool = options.payload['mode'] & 2; const result = []; for (let i = 0; i < options.payload.numoftrans; i++) { const entry = { transitionTime: this.readUInt16() }; if (heat) { entry.heatSetpoint = this.readUInt16(); } if (cool) { entry.coolSetpoint = this.readUInt16(); } result.push(entry); } return result; } writeListThermoTransitions(value) { for (const entry of value) { this.writeUInt16(entry.transitionTime); if (entry.hasOwnProperty('heatSetpoint')) { this.writeUInt16(entry.heatSetpoint); } if (entry.hasOwnProperty('coolSetpoint')) { this.writeUInt16(entry.coolSetpoint); } } } readGdpFrame(options) { // Commisioning if (options.payload.commandID === 0xE0) { const frame = { deviceID: this.readUInt8(), options: this.readUInt8(), extendedOptions: 0, securityKey: Buffer.alloc(16), keyMic: 0, outgoingCounter: 0, applicationInfo: 0, manufacturerID: 0, modelID: 0, numGdpCommands: 0, gpdCommandIdList: Buffer.alloc(0), numServerClusters: 0, numClientClusters: 0, gpdServerClusters: Buffer.alloc(0), gpdClientClusters: Buffer.alloc(0), }; if (frame.options & 0x80) { frame.extendedOptions = this.readUInt8(); } if (frame.extendedOptions & 0x20) { frame.securityKey = this.readBuffer(16); } if (frame.extendedOptions & 0x40) { frame.keyMic = this.readUInt32(); } if (frame.extendedOptions & 0x80) { frame.outgoingCounter = this.readUInt32(); } if (frame.options & 0x04) { frame.applicationInfo = this.readUInt8(); } if (frame.applicationInfo & 0x01) { frame.manufacturerID = this.readUInt16(); } if (frame.applicationInfo & 0x02) { frame.modelID = this.readUInt16(); } if (frame.applicationInfo & 0x04) { frame.numGdpCommands = this.readUInt8(); frame.gpdCommandIdList = this.readBuffer(frame.numGdpCommands); } if (frame.applicationInfo & 0x08) { const len = this.readUInt8(); frame.numServerClusters = len & 0xF; frame.numClientClusters = (len >> 4) & 0xF; frame.gpdServerClusters = this.readBuffer(2 * frame.numServerClusters); frame.gpdClientClusters = this.readBuffer(2 * frame.numClientClusters); } return frame; // Channel Request } else if (options.payload.commandID === 0xE3) { const options = this.readUInt8(); return { nextChannel: options & 0xF, nextNextChannel: options >> 4 }; // Manufacturer-specific Attribute Reporting } else if (options.payload.commandID == 0xA1) { const start = this.position; const frame = { manufacturerCode: this.readUInt16(), clusterID: this.readUInt16(), attributes: {}, }; const cluster = Utils.getCluster(frame.clusterID, frame.manufacturerCode); while (this.position - start < options.payload.payloadSize) { const attributeID = this.readUInt16(); const type = this.readUInt8(); let attribute = attributeID; try { attribute = cluster.getAttribute(attributeID).name; } catch { logger_1.logger.info("Unknown attribute " + attributeID + " in cluster " + cluster.name, NS); } frame.attributes[attribute] = this.read(definition_1.DataType[type], options); } return frame; } else if (this.position != this.buffer.length) { return { raw: this.buffer.slice(this.position) }; } else { return {}; } } writeGdpFrame(value) { if (value.commandID == 0xF0) { // Commissioning Reply const v = value; const panIDPresent = v.options & (1 << 0); const gpdSecurityKeyPresent = v.options & (1 << 1); const gpdKeyEncryption = v.options & (1 << 2); const securityLevel = v.options & (3 << 3) >> 3; const hasGPDKeyMIC = gpdKeyEncryption && gpdSecurityKeyPresent; const hasFrameCounter = gpdSecurityKeyPresent && gpdKeyEncryption && (securityLevel === 0b10 || securityLevel === 0b11); this.writeUInt8(1 + (panIDPresent ? 2 : 0) + (gpdSecurityKeyPresent ? 16 : 0) + (hasGPDKeyMIC ? 4 : 0) + (hasFrameCounter ? 4 : 0)); // Length this.writeUInt8(v.options); if (panIDPresent) { this.writeUInt16(v.panID); } if (gpdSecurityKeyPresent) { this.writeBuffer(v.securityKey, 16); } if (hasGPDKeyMIC) { this.writeUInt32(v.keyMic); } if (hasFrameCounter) { this.writeUInt32(v.frameCounter); } } else if (value.commandID == 0xF3) { // Channel configuration const v = value; this.writeUInt8(1); this.writeUInt8(v.operationalChannel & 0xF | ((v.basic ? 1 : 0) << 4)); } else if (value.commandID == 0xF4 || value.commandID == 0xF5 || (value.commandID >= 0xF7 && value.commandID <= 0xFF)) { // Other commands sent to GPD const v = value; this.writeUInt8(v.buffer.length); this.writeBuffer(v.buffer, v.buffer.length); } } readListTuyaDataPointValues() { const value = []; while (this.isMore()) { try { const dp = this.readUInt8(); const datatype = this.readUInt8(); const len_hi = this.readUInt8(); const len_lo = this.readUInt8(); const data = this.readBuffer(len_lo + (len_hi << 8)); value.push({ dp, datatype, data }); } catch (error) { break; } } return value; } writeListTuyaDataPointValues(dpValues) { for (const dpValue of dpValues) { this.writeUInt8(dpValue.dp); this.writeUInt8(dpValue.datatype); const dataLen = dpValue.data.length; this.writeUInt8((dataLen >> 8) & 0xFF); this.writeUInt8(dataLen & 0xFF); this.writeBuffer(dpValue.data, dataLen); } } readListMiboxerZones() { const value = []; const len = this.readUInt8(); for (let i = 0; i < len; i++) { value.push({ groupId: this.readUInt16(), zoneNum: this.readUInt8(), }); } return value; } readBigEndianUInt24() { return this.readBuffer(3).readUIntBE(0, 3); } writeListMiboxerZones(values) { this.writeUInt8(values.length); for (const value of values) { this.writeUInt16(value.groupId); this.writeUInt8(value.zoneNum); } } writeBigEndianUInt24(value) { const buffer = Buffer.alloc(3); buffer.writeUIntLE(value, 0, 3); this.writeBuffer(buffer.reverse(), 3); } readUInt40() { const lsb = this.readUInt32(); const msb = this.readUInt8(); return [msb, lsb]; } writeUInt40(value) { this.writeUInt32(value[1]); this.writeUInt8(value[0]); } readUInt48() { const lsb = this.readUInt32(); const msb = this.readUInt16(); return [msb, lsb]; } writeUInt48(value) { this.writeUInt32(value[1]); this.writeUInt16(value[0]); } readUInt56() { const lsb = this.readUInt32(); const xsb = this.readUInt16(); const msb = this.readUInt8(); return [msb, xsb, lsb]; } writeUInt56(value) { const temp = Buffer.alloc(8); temp.writeUInt32LE(value[1], 0); temp.writeUInt32LE(value[0], 4); this.writeBuffer(temp.slice(0, 7), 7); } readUInt64() { return this.readIeeeAddr(); } writeUInt64(value) { const msb = parseInt(value.slice(2, 10), 16); const lsb = parseInt(value.slice(10), 16); this.writeUInt32(lsb); this.writeUInt32(msb); } writeStructuredSelector(value) { if (value != null) { const indexes = value.indexes || []; const indicatorType = value.indicatorType || tstype_1.StructuredIndicatorType.WriteWhole; const indicator = indexes.length + indicatorType; this.writeUInt8(indicator); for (const index of indexes) { this.writeUInt16(index); } } } write(type, value, options) { // TODO: write for the following is missing: struct type = aliases[type] || type; if (type === 'uint40') { return this.writeUInt40(value); } else if (type === 'EXTENSION_FIELD_SETS') { return this.writeExtensionFieldSets(value); } else if (type === 'LIST_ZONEINFO') { return this.writeListZoneInfo(value); } else if (type === 'LIST_THERMO_TRANSITIONS') { return this.writeListThermoTransitions(value); } else if (type === 'LIST_TUYA_DATAPOINT_VALUES') { return this.writeListTuyaDataPointValues(value); } else if (type === 'LIST_MIBOXER_ZONES') { return this.writeListMiboxerZones(value); } else if (type === 'BIG_ENDIAN_UINT24') { return this.writeBigEndianUInt24(value); } else if (type === 'GDP_FRAME') { return this.writeGdpFrame(value); } else if (type === 'uint48') { return this.writeUInt48(value); } else if (type === 'uint56') { return this.writeUInt56(value); } else if (type === 'uint64') { return this.writeUInt64(value); } else if (type === 'charStr') { return this.writeCharStr(value); } else if (type === 'longCharStr') { return this.writeLongCharStr(value); } else if (type === 'octetStr') { return this.writeOctetStr(value); } else if (type === 'array') { return this.writeArray(value); } else if (type === 'USE_DATA_TYPE') { return this.writeUseDataType(value, options); } else if (type == 'STRUCTURED_SELECTOR') { return this.writeStructuredSelector(value); } else { // In case the type is undefined, write it as a buffer to easily allow for custom types // e.g. for https://github.com/Koenkk/zigbee-herdsman/issues/127 type = type === undefined ? 'BUFFER' : type; // TODO: remove uppercase once dataTypes are snake case return super.write(type.toUpperCase(), value, options); } } read(type, options) { type = aliases[type] || type; if (type === 'USE_DATA_TYPE') { return this.readUseDataType(options); } else if (type === 'EXTENSION_FIELD_SETS') { return this.readExtensionFieldSets(); } else if (type === 'LIST_ZONEINFO') { return this.readListZoneInfo(options); } else if (type === 'LIST_THERMO_TRANSITIONS') { return this.readListThermoTransitions(options); } else if (type === 'GDP_FRAME') { return this.readGdpFrame(options); } else if (type === 'LIST_TUYA_DATAPOINT_VALUES') { return this.readListTuyaDataPointValues(); } else if (type === 'LIST_MIBOXER_ZONES') { return this.readListMiboxerZones(); } else if (type === 'BIG_ENDIAN_UINT24') { return this.readBigEndianUInt24(); } else if (type === 'uint40') { return this.readUInt40(); } else if (type === 'uint48') { return this.readUInt48(); } else if (type === 'uint56') { return this.readUInt56(); } else if (type === 'uint64') { return this.readUInt64(); } else if (type === 'octetStr') { return this.readOctetStr(); } else if (type === 'charStr') { return this.readCharStr(options); } else if (type === 'longCharStr') { return this.readLongCharStr(); } else if (type === 'array') { return this.readArray(); } else if (type === 'struct') { return this.readStruct(); } else if (type === 'BUFFER') { return this.readBuffer(this.buffer.length); } else { // TODO: remove uppercase once dataTypes are snake case return super.read(type.toUpperCase(), options); } } } exports.default = BuffaloZcl; //# sourceMappingURL=buffaloZcl.js.map