zigbee-herdsman-zigate
Version:
An open source ZigBee gateway solution with node.js.
385 lines (317 loc) • 15.3 kB
text/typescript
import {Direction, Foundation, DataType, BuffaloZclDataType, FrameControl} from './definition';
import * as Utils from './utils';
import BuffaloZcl from './buffaloZcl';
import {TsType as BuffaloTsType} from '../buffalo';
import * as TsType from './tstype';
import {TsType as DefinitionTsType, FrameType} from './definition';
const MINIMAL_FRAME_LENGTH = 3;
// eslint-disable-next-line
type ZclPayload = any;
interface ZclHeader {
frameControl: FrameControl;
manufacturerCode: number;
transactionSequenceNumber: number;
commandIdentifier: number;
}
const ListTypes: number[] = [
BuffaloZclDataType.LIST_UINT8,
BuffaloZclDataType.LIST_UINT16,
BuffaloZclDataType.LIST_UINT24,
BuffaloZclDataType.LIST_UINT32,
BuffaloZclDataType.LIST_ZONEINFO,
];
class ZclFrame {
public readonly Header: ZclHeader;
public readonly Payload: ZclPayload;
public readonly Cluster: TsType.Cluster;
private constructor(header: ZclHeader, payload: ZclPayload, cluster: TsType.Cluster) {
this.Header = header;
this.Payload = payload;
this.Cluster = cluster;
}
/**
* Creating
*/
public static create(
frameType: FrameType, direction: Direction, disableDefaultResponse: boolean, manufacturerCode: number,
transactionSequenceNumber: number, commandKey: number | string, clusterID: number,
payload: ZclPayload, reservedBits = 0
): ZclFrame {
const cluster = Utils.getCluster(clusterID, manufacturerCode != null ? manufacturerCode : null);
let command: TsType.Command = null;
if (frameType === FrameType.GLOBAL) {
command = Utils.getGlobalCommand(commandKey);
} else {
command = direction === Direction.CLIENT_TO_SERVER ?
cluster.getCommand(commandKey) : cluster.getCommandResponse(commandKey);
}
const header: ZclHeader = {
frameControl: {
reservedBits, frameType, direction, disableDefaultResponse,
manufacturerSpecific: manufacturerCode !== null,
},
transactionSequenceNumber,
manufacturerCode,
commandIdentifier: command.ID,
};
return new ZclFrame(header, payload, cluster);
}
public toBuffer(): Buffer {
const buffalo = new BuffaloZcl(Buffer.alloc(250));
this.writeHeader(buffalo);
if (this.Header.frameControl.frameType === FrameType.GLOBAL) {
this.writePayloadGlobal(buffalo);
} else if (this.Header.frameControl.frameType === FrameType.SPECIFIC) {
this.writePayloadCluster(buffalo);
} else {
throw new Error(`Frametype '${this.Header.frameControl.frameType}' not valid`);
}
return buffalo.getBuffer().slice(0, buffalo.getPosition());
}
private writeHeader(buffalo: BuffaloZcl): void {
const frameControl = (
(this.Header.frameControl.frameType & 0x03) |
(((this.Header.frameControl.manufacturerSpecific ? 1 : 0) << 2) & 0x04) |
((this.Header.frameControl.direction << 3) & 0x08) |
(((this.Header.frameControl.disableDefaultResponse ? 1 : 0) << 4) & 0x10) |
((this.Header.frameControl.reservedBits << 5) & 0xE0)
);
buffalo.writeUInt8(frameControl);
if (this.Header.frameControl.manufacturerSpecific) {
buffalo.writeUInt16(this.Header.manufacturerCode);
}
buffalo.writeUInt8(this.Header.transactionSequenceNumber);
buffalo.writeUInt8(this.Header.commandIdentifier);
}
private writePayloadGlobal(buffalo: BuffaloZcl): void {
const command = Object.values(Foundation).find((c): boolean => c.ID === this.Header.commandIdentifier);
if (command.parseStrategy === 'repetitive') {
for (const entry of this.Payload) {
for (const parameter of command.parameters) {
const options: TsType.BuffaloZclOptions = {};
if (!ZclFrame.conditionsValid(parameter, entry, null)) {
continue;
}
if (parameter.type === BuffaloZclDataType.USE_DATA_TYPE && typeof entry.dataType === 'number') {
// We need to grab the dataType to parse useDataType
options.dataType = DataType[entry.dataType];
}
const typeStr = ZclFrame.getDataTypeString(parameter.type);
buffalo.write(typeStr, entry[parameter.name], options);
}
}
} else if (command.parseStrategy === 'flat') {
for (const parameter of command.parameters) {
buffalo.write(DataType[parameter.type], this.Payload[parameter.name], {});
}
} else {
/* istanbul ignore else */
if (command.parseStrategy === 'oneof') {
/* istanbul ignore else */
if (command === Foundation.discoverRsp) {
buffalo.writeUInt8(this.Payload.discComplete);
for (const entry of this.Payload.attrInfos) {
for (const parameter of command.parameters) {
buffalo.write(DataType[parameter.type], entry[parameter.name], {});
}
}
}
}
}
}
private writePayloadCluster(buffalo: BuffaloZcl): void {
const command = this.Header.frameControl.direction === Direction.CLIENT_TO_SERVER ?
this.Cluster.getCommand(this.Header.commandIdentifier) :
this.Cluster.getCommandResponse(this.Header.commandIdentifier);
for (const parameter of command.parameters) {
if (!ZclFrame.conditionsValid(parameter, this.Payload, null)) {
continue;
}
if (!this.Payload.hasOwnProperty(parameter.name)) {
throw new Error(`Parameter '${parameter.name}' is missing`);
}
const typeStr = ZclFrame.getDataTypeString(parameter.type);
buffalo.write(typeStr, this.Payload[parameter.name], {});
}
}
/**
* Parsing
*/
public static fromBuffer(clusterID: number, buffer: Buffer): ZclFrame {
if (buffer.length < MINIMAL_FRAME_LENGTH) {
throw new Error("ZclFrame length is lower than minimal length");
}
const buffalo = new BuffaloZcl(buffer);
const header = this.parseHeader(buffalo);
const cluster = Utils.getCluster(
clusterID,
header.frameControl.manufacturerSpecific ? header.manufacturerCode : null
);
const payload = this.parsePayload(header, cluster, buffalo);
return new ZclFrame(header, payload, cluster);
}
private static parseHeader(buffalo: BuffaloZcl): ZclHeader {
const frameControlValue = buffalo.readUInt8();
const frameControl = {
frameType: frameControlValue & 0x03,
manufacturerSpecific: ((frameControlValue >> 2) & 0x01) === 1,
direction: (frameControlValue >> 3) & 0x01,
disableDefaultResponse: ((frameControlValue >> 4) & 0x01) === 1,
reservedBits: frameControlValue >> 5,
};
let manufacturerCode = null;
if (frameControl.manufacturerSpecific) {
manufacturerCode = buffalo.readUInt16();
}
const transactionSequenceNumber = buffalo.readUInt8();
const commandIdentifier = buffalo.readUInt8();
return {frameControl, transactionSequenceNumber, manufacturerCode, commandIdentifier};
}
private static parsePayload(header: ZclHeader, cluster: TsType.Cluster, buffalo: BuffaloZcl): ZclPayload {
if (header.frameControl.frameType === FrameType.GLOBAL) {
return this.parsePayloadGlobal(header, buffalo);
} else if (header.frameControl.frameType === FrameType.SPECIFIC) {
return this.parsePayloadCluster(header, cluster, buffalo);
} else {
throw new Error(`Unsupported frameType '${header.frameControl.frameType}'`);
}
}
private static parsePayloadCluster(header: ZclHeader, cluster: TsType.Cluster, buffalo: BuffaloZcl): ZclPayload {
const command = header.frameControl.direction === Direction.CLIENT_TO_SERVER ?
cluster.getCommand(header.commandIdentifier) : cluster.getCommandResponse(header.commandIdentifier);
const payload: ZclPayload = {};
for (const parameter of command.parameters) {
const options: BuffaloTsType.Options = {payload};
if (ListTypes.includes(parameter.type)) {
const lengthParameter = command.parameters[command.parameters.indexOf(parameter) - 1];
const length = payload[lengthParameter.name];
/* istanbul ignore else */
if (typeof length === 'number') {
options.length = length;
}
}
const typeStr = ZclFrame.getDataTypeString(parameter.type);
payload[parameter.name] = buffalo.read(typeStr, options);
}
return payload;
}
private static parsePayloadGlobal(header: ZclHeader, buffalo: BuffaloZcl): ZclPayload {
const command = Object.values(Foundation).find((c): boolean => c.ID === header.commandIdentifier);
if (command.parseStrategy === 'repetitive') {
const payload = [];
while (buffalo.getPosition() < buffalo.getBuffer().length) {
const entry: {[s: string]: BuffaloTsType.Value} = {};
for (const parameter of command.parameters) {
const options: TsType.BuffaloZclOptions = {};
if (!this.conditionsValid(parameter, entry, buffalo.getBuffer().length - buffalo.getPosition())) {
continue;
}
if (parameter.type === BuffaloZclDataType.USE_DATA_TYPE && typeof entry.dataType === 'number') {
// We need to grab the dataType to parse useDataType
options.dataType = DataType[entry.dataType];
if (entry.dataType === DataType.charStr && entry.hasOwnProperty('attrId')) {
// For Xiaomi struct parsing we need to pass the attributeID.
options.attrId = entry.attrId;
}
}
const typeStr = DataType[parameter.type] != null ?
DataType[parameter.type] : BuffaloZclDataType[parameter.type];
entry[parameter.name] = buffalo.read(typeStr, options);
// TODO: not needed, but temp workaroudn to make payload equal to that of zcl-packet
if (parameter.type === BuffaloZclDataType.USE_DATA_TYPE && entry.dataType === DataType.struct) {
entry['structElms'] = entry.attrData;
entry['numElms'] = entry.attrData.length;
}
}
payload.push(entry);
}
return payload;
} else if (command.parseStrategy === 'flat') {
const payload: {[s: string]: BuffaloTsType.Value} = {};
for (const parameter of command.parameters) {
payload[parameter.name] = buffalo.read(DataType[parameter.type], {});
}
return payload;
} else {
/* istanbul ignore else */
if (command.parseStrategy === 'oneof') {
/* istanbul ignore else */
if (command === Foundation.discoverRsp) {
const payload: {[s: string]: BuffaloTsType.Value} = {};
payload.discComplete = buffalo.readUInt8();
payload.attrInfos = [];
while (buffalo.getPosition() < buffalo.getBuffer().length) {
const entry: {[s: string]: BuffaloTsType.Value} = {};
for (const parameter of command.parameters) {
entry[parameter.name] = buffalo.read(DataType[parameter.type], {});
}
payload.attrInfos.push(entry);
}
return payload;
}
}
}
}
/**
* Utils
*/
private static getDataTypeString(dataType: DataType | BuffaloZclDataType): string {
return DataType[dataType] != null ? DataType[dataType] : BuffaloZclDataType[dataType];
}
private static conditionsValid(
parameter: DefinitionTsType.ParameterDefinition,
entry: ZclPayload,
remainingBufferBytes: number
): boolean {
if (parameter.conditions) {
const failedCondition = parameter.conditions.find((condition): boolean => {
if (condition.type === 'statusEquals') {
return entry.status !== condition.value;
} else if (condition.type == 'statusNotEquals') {
return entry.status === condition.value;
} else if (condition.type == 'directionEquals') {
return entry.direction !== condition.value;
} else if (remainingBufferBytes !== null && condition.type == 'minimumRemainingBufferBytes') {
return remainingBufferBytes < condition.value;
} else {
/* istanbul ignore else */
if (condition.type == 'dataTypeValueTypeEquals') {
return Utils.IsDataTypeAnalogOrDiscrete(entry.dataType) !== condition.value;
}
}
});
if (failedCondition) {
return false;
}
}
return true;
}
public isSpecific(): boolean {
return this.Header.frameControl.frameType === FrameType.SPECIFIC;
}
public isGlobal(): boolean {
return this.Header.frameControl.frameType === FrameType.GLOBAL;
}
// List of clusters is not completed, feel free to add more.
public isCluster(clusterName: 'genTime' | 'genAnalogInput' | 'genBasic' | 'genGroups' | 'ssIasZone'): boolean {
return this.Cluster.name === clusterName;
}
// List of commands is not completed, feel free to add more.
public isCommand(
commandName: 'read' | 'report' | 'readRsp' | 'remove' | 'add' | 'write' | 'enrollReq' | 'configReport'
): boolean {
return this.getCommand().name === commandName;
}
public getCommand(): TsType.Command {
let command: TsType.Command = null;
if (this.Header.frameControl.frameType === FrameType.GLOBAL) {
command = Utils.getGlobalCommand(this.Header.commandIdentifier);
} else {
command = this.Header.frameControl.direction === Direction.CLIENT_TO_SERVER ?
this.Cluster.getCommand(this.Header.commandIdentifier) :
this.Cluster.getCommandResponse(this.Header.commandIdentifier);
}
return command;
}
}
export default ZclFrame;