zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
589 lines • 20.6 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;
};
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