import {Buffalo, TsType} from '../buffalo'; import {DataType} from './definition'; import {BuffaloZclOptions, ZclArray} from './tstype'; const aliases: {[s: string]: string} = { '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', }; interface ThermoTransition {transitionTime: number; heatSetpoint?: number; coolSetpoint?: number} class BuffaloZcl extends Buffalo { private readUseDataType(options: BuffaloZclOptions): TsType.Value { return this.read(options.dataType, options); } private writeUseDataType(value: string, options: BuffaloZclOptions): void { return this.write(options.dataType, value, options); } private readArray(): TsType.Value { const values: TsType.Value = []; const elementType = DataType[this.readUInt8()]; const numberOfElements = this.readUInt16(); for (let i = 0; i < numberOfElements; i++) { const value = this.read(elementType, {}); values.push(value); } return values; } private writeArray(value: ZclArray): void { const elTypeNumeric = typeof value.elementType === 'number' ? value.elementType : DataType[value.elementType]; this.writeUInt8(elTypeNumeric); this.writeUInt16(value.elements.length); value.elements.forEach(element => { this.write(DataType[elTypeNumeric], element, {}); }); } private readStruct(): TsType.Value { const values: TsType.Value = []; const numberOfElements = this.readUInt16(); for (let i = 0; i < numberOfElements; i++) { const elementType = this.readUInt8(); const value = this.read(DataType[elementType], {}); values.push({elmType: elementType, elmVal: value}); } return values; } private readOctetStr(): TsType.Value { const length = this.readUInt8(); const value = this.buffer.slice(this.position, this.position + length); this.position += length; return value; } private readCharStr(options: BuffaloZclOptions): TsType.Value { const length = this.readUInt8(); if (options.attrId === 65281) { const value: {[i: number]: number|number[]} = {}; // Xiaomi struct parsing for (let i = 0; i < length; i++) { const index = this.readUInt8(); const dataType = DataType[this.readUInt8()]; value[index] = this.read(dataType, {}); if (this.position === this.buffer.length) { break; } } return value; } else { const value = this.buffer.toString('utf8', this.position, this.position + length); this.position += length; return value; } } private writeCharStr(value: string | number[]): void { if (typeof value === 'string') { this.writeUInt8(value.length); this.position += this.buffer.write(value, this.position, 'utf8'); } else { this.writeBuffer(value, value.length); } } private readLongCharStr(): TsType.Value { const length = this.readUInt16(); const value = this.buffer.toString('utf8', this.position, this.position + length); this.position += length; return value; } private writeLongCharStr(value: string): void { this.writeUInt16(value.length); this.position += this.buffer.write(value, this.position, 'utf8'); } private writeOctetStr(value: number[]): void { this.writeUInt8(value.length); this.writeBuffer(value, value.length); } private readExtensionFielSets(): TsType.Value { const value = []; while (this.position < this.buffer.length) { const clstId = this.readUInt16(); const len = this.readUInt8(); const extField = []; for (let k = 0; k < len; k++) { extField.push(this.readUInt8()); } value.push({extField, clstId, len}); } return value; } private writeExtensionFieldSets(values: {clstId: number; len: number; extField: number[]}[]): void { for (const value of values) { this.writeUInt16(value.clstId); this.writeUInt8(value.len); for (const entry of value.extField) { this.writeUInt8(entry); } } } private writeListZoneInfo(values: {zoneID: number; zoneStatus: number}[]): void { for (const value of values) { this.writeUInt8(value.zoneID); this.writeUInt16(value.zoneStatus); } } private readListZoneInfo(options: TsType.Options): TsType.Value { const value = []; for (let i = 0; i < options.length; i++) { value.push({ zoneID: this.readUInt8(), zoneStatus: this.readUInt16(), }); } return value; } private readListThermoTransitions(options: TsType.Options): TsType.Value { 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: ThermoTransition = {transitionTime: this.readUInt16()}; if (heat) { entry.heatSetpoint = this.readUInt16(); } if (cool) { entry.coolSetpoint = this.readUInt16(); } result.push(entry); } return result; } private writeListThermoTransitions(value: ThermoTransition[]): void { 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); } } } private readGdpFrame(options: TsType.Options): TsType.Value { // Commisioning if (options.payload.commandID === 224) { return { deviceID: this.readUInt8(), options: this.readUInt8(), extendedOptions: this.readUInt8(), securityKey: this.readBuffer(16), keyMic: this.readUInt32(), outgoingCounter: this.readUInt32(), }; } else if (this.position != this.buffer.length) { return {raw: this.buffer.slice(this.position)}; } else { return {}; } } private readUInt40(): TsType.Value { const lsb = this.readUInt32(); const msb = this.readUInt8(); return [msb, lsb]; } private writeUInt40(value: number[]): void { this.writeUInt32(value[1]); this.writeUInt8(value[0]); } private readUInt48(): TsType.Value { const lsb = this.readUInt32(); const msb = this.readUInt16(); return [msb, lsb]; } private writeUInt48(value: number[]): void { this.writeUInt32(value[1]); this.writeUInt16(value[0]); } private readUInt56(): TsType.Value { const lsb = this.readUInt32(); const xsb = this.readUInt16(); const msb = this.readUInt8(); return [msb, xsb, lsb]; } private writeUInt56(value: number[]): void { const temp = Buffer.alloc(8); temp.writeUInt32LE(value[1], 0); temp.writeUInt32LE(value[0], 4); this.writeBuffer(temp.slice(0, 7), 7); } private readUInt64(): TsType.Value { return this.readIeeeAddr(); } private writeUInt64(value: string): void { const msb = parseInt(value.slice(2,10), 16); const lsb = parseInt(value.slice(10), 16); this.writeUInt32(lsb); this.writeUInt32(msb); } public write(type: string, value: TsType.Value, options: BuffaloZclOptions): void { // 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 === '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 { // 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); } } public read(type: string, options: BuffaloZclOptions): TsType.Value { type = aliases[type] || type; if (type === 'USE_DATA_TYPE') { return this.readUseDataType(options); } else if (type === 'EXTENSION_FIELD_SETS') { return this.readExtensionFielSets(); } 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 === '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 { // TODO: remove uppercase once dataTypes are snake case return super.read(type.toUpperCase(), options); } } } export default BuffaloZcl;