zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
337 lines (278 loc) • 13.3 kB
text/typescript
import "../../utils/patchBigIntSerialization";
import {BuffaloZcl} from "./buffaloZcl";
import {BuffaloZclDataType, DataType, Direction, FrameType, ParameterCondition} from "./definition/enums";
import type {FoundationCommandName} from "./definition/foundation";
import type {Status} from "./definition/status";
import type {BuffaloZclOptions, Cluster, ClusterName, Command, CustomClusters, ParameterDefinition} from "./definition/tstype";
import * as Utils from "./utils";
import {ZclHeader} from "./zclHeader";
// biome-ignore lint/suspicious/noExplicitAny: API
type ZclPayload = any;
const ListTypes: number[] = [
BuffaloZclDataType.LIST_UINT8,
BuffaloZclDataType.LIST_UINT16,
BuffaloZclDataType.LIST_UINT24,
BuffaloZclDataType.LIST_UINT32,
BuffaloZclDataType.LIST_ZONEINFO,
];
export class ZclFrame {
public readonly header: ZclHeader;
public readonly payload: ZclPayload;
public readonly cluster: Cluster;
public readonly command: Command;
private constructor(header: ZclHeader, payload: ZclPayload, cluster: Cluster, command: Command) {
this.header = header;
this.payload = payload;
this.cluster = cluster;
this.command = command;
}
public toString(): string {
return JSON.stringify({header: this.header, payload: this.payload, command: this.command});
}
/**
* Creating
*/
public static create(
frameType: FrameType,
direction: Direction,
disableDefaultResponse: boolean,
manufacturerCode: number | undefined,
transactionSequenceNumber: number,
commandKey: number | string,
clusterKey: number | string,
payload: ZclPayload,
customClusters: CustomClusters,
reservedBits = 0,
): ZclFrame {
const cluster = Utils.getCluster(clusterKey, manufacturerCode, customClusters);
const command: Command =
frameType === FrameType.GLOBAL
? Utils.getGlobalCommand(commandKey)
: direction === Direction.CLIENT_TO_SERVER
? cluster.getCommand(commandKey)
: cluster.getCommandResponse(commandKey);
const header = new ZclHeader(
{reservedBits, frameType, direction, disableDefaultResponse, manufacturerSpecific: manufacturerCode != null},
manufacturerCode,
transactionSequenceNumber,
command.ID,
);
return new ZclFrame(header, payload, cluster, command);
}
public toBuffer(): Buffer {
const buffalo = new BuffaloZcl(Buffer.alloc(250));
this.header.write(buffalo);
if (this.header.isGlobal) {
this.writePayloadGlobal(buffalo);
} else if (this.header.isSpecific) {
this.writePayloadCluster(buffalo);
} else {
throw new Error(`Frametype '${this.header.frameControl.frameType}' not valid`);
}
return buffalo.getWritten();
}
private writePayloadGlobal(buffalo: BuffaloZcl): void {
const command = Utils.getFoundationCommand(this.command.ID);
if (command.parseStrategy === "repetitive") {
for (const entry of this.payload) {
for (const parameter of command.parameters) {
const options: BuffaloZclOptions = {};
if (!ZclFrame.conditionsValid(parameter, entry, undefined)) {
continue;
}
if (parameter.type === BuffaloZclDataType.USE_DATA_TYPE && typeof entry.dataType === "number") {
// We need to grab the dataType to parse useDataType
options.dataType = entry.dataType;
}
buffalo.write(parameter.type, entry[parameter.name], options);
}
}
} else if (command.parseStrategy === "flat") {
for (const parameter of command.parameters) {
buffalo.write(parameter.type, this.payload[parameter.name], {});
}
} else {
if (command.parseStrategy === "oneof") {
if (Utils.isFoundationDiscoverRsp(command.ID)) {
buffalo.writeUInt8(this.payload.discComplete);
for (const entry of this.payload.attrInfos) {
for (const parameter of command.parameters) {
buffalo.write(parameter.type, entry[parameter.name], {});
}
}
}
}
}
}
private writePayloadCluster(buffalo: BuffaloZcl): void {
for (const parameter of this.command.parameters) {
if (!ZclFrame.conditionsValid(parameter, this.payload, undefined)) {
continue;
}
// TODO: biome migration - safer
if (this.payload[parameter.name] == null) {
throw new Error(`Parameter '${parameter.name}' is missing`);
}
buffalo.write(parameter.type, this.payload[parameter.name], {});
}
}
/**
* Parsing
*/
public static fromBuffer(clusterID: number, header: ZclHeader | undefined, buffer: Buffer, customClusters: CustomClusters): ZclFrame {
if (!header) {
throw new Error("Invalid ZclHeader.");
}
const buffalo = new BuffaloZcl(buffer, header.length);
const cluster = Utils.getCluster(clusterID, header.manufacturerCode, customClusters);
const command: Command = header.isGlobal
? Utils.getGlobalCommand(header.commandIdentifier)
: header.frameControl.direction === Direction.CLIENT_TO_SERVER
? cluster.getCommand(header.commandIdentifier)
: cluster.getCommandResponse(header.commandIdentifier);
const payload = ZclFrame.parsePayload(header, cluster, buffalo);
return new ZclFrame(header, payload, cluster, command);
}
private static parsePayload(header: ZclHeader, cluster: Cluster, buffalo: BuffaloZcl): ZclPayload {
if (header.isGlobal) {
return ZclFrame.parsePayloadGlobal(header, buffalo);
}
if (header.isSpecific) {
return ZclFrame.parsePayloadCluster(header, cluster, buffalo);
}
throw new Error(`Unsupported frameType '${header.frameControl.frameType}'`);
}
private static parsePayloadCluster(header: ZclHeader, cluster: 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: BuffaloZclOptions = {payload};
if (!ZclFrame.conditionsValid(parameter, payload, buffalo.getBuffer().length - buffalo.getPosition())) {
continue;
}
if (ListTypes.includes(parameter.type)) {
const lengthParameter = command.parameters[command.parameters.indexOf(parameter) - 1];
const length = payload[lengthParameter.name];
if (typeof length === "number") {
options.length = length;
}
}
payload[parameter.name] = buffalo.read(parameter.type, options);
}
return payload;
}
private static parsePayloadGlobal(header: ZclHeader, buffalo: BuffaloZcl): ZclPayload {
const command = Utils.getFoundationCommand(header.commandIdentifier);
if (command.parseStrategy === "repetitive") {
const payload = [];
while (buffalo.isMore()) {
// biome-ignore lint/suspicious/noExplicitAny: API
const entry: {[s: string]: any} = {};
for (const parameter of command.parameters) {
const options: BuffaloZclOptions = {};
if (!ZclFrame.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 = entry.dataType;
if (entry.dataType === DataType.CHAR_STR && entry.attrId === 65281) {
// [workaround] parse char str as Xiaomi struct
options.dataType = BuffaloZclDataType.MI_STRUCT;
}
}
entry[parameter.name] = buffalo.read(parameter.type, options);
// TODO: not needed, but temp workaroudn to make payload equal to that of zcl-packet
// XXX: is this still needed?
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;
}
if (command.parseStrategy === "flat") {
// biome-ignore lint/suspicious/noExplicitAny: API
const payload: {[s: string]: any} = {};
for (const parameter of command.parameters) {
payload[parameter.name] = buffalo.read(parameter.type, {});
}
return payload;
}
if (command.parseStrategy === "oneof") {
if (Utils.isFoundationDiscoverRsp(command.ID)) {
// biome-ignore lint/suspicious/noExplicitAny: API
const payload: {discComplete: number; attrInfos: {[k: string]: any}[]} = {
discComplete: buffalo.readUInt8(),
attrInfos: [],
};
while (buffalo.isMore()) {
const entry: (typeof payload.attrInfos)[number] = {};
for (const parameter of command.parameters) {
entry[parameter.name] = buffalo.read(parameter.type, {});
}
payload.attrInfos.push(entry);
}
return payload;
}
}
}
/**
* Utils
*/
public static conditionsValid(parameter: ParameterDefinition, entry: ZclPayload, remainingBufferBytes: number | undefined): boolean {
if (parameter.conditions) {
for (const condition of parameter.conditions) {
switch (condition.type) {
case ParameterCondition.STATUS_EQUAL: {
if ((entry.status as Status) !== condition.value) return false;
break;
}
case ParameterCondition.STATUS_NOT_EQUAL: {
if ((entry.status as Status) === condition.value) return false;
break;
}
case ParameterCondition.DIRECTION_EQUAL: {
if ((entry.direction as Direction) !== condition.value) return false;
break;
}
case ParameterCondition.BITMASK_SET: {
if (condition.reversed) {
if ((entry[condition.param] & condition.mask) === condition.mask) return false;
} else if ((entry[condition.param] & condition.mask) !== condition.mask) return false;
break;
}
case ParameterCondition.BITFIELD_ENUM: {
if (((entry[condition.param] >> condition.offset) & ((1 << condition.size) - 1)) !== condition.value) return false;
break;
}
case ParameterCondition.MINIMUM_REMAINING_BUFFER_BYTES: {
if (remainingBufferBytes !== undefined && remainingBufferBytes < condition.value) return false;
break;
}
case ParameterCondition.DATA_TYPE_CLASS_EQUAL: {
if (Utils.getDataTypeClass(entry.dataType) !== condition.value) return false;
break;
}
case ParameterCondition.FIELD_EQUAL: {
if (entry[condition.field] !== condition.value) return false;
break;
}
}
}
}
return true;
}
public isCluster(clusterName: FoundationCommandName | ClusterName): boolean {
return this.cluster.name === clusterName;
}
// List of commands is not completed, feel free to add more.
public isCommand(commandName: FoundationCommandName | "remove" | "add" | "write" | "enrollReq" | "checkin" | "getAlarm" | "arm"): boolean {
return this.command.name === commandName;
}
}