import {describe, expect, it} from "vitest"; import * as Zcl from "../../../src/zspec/zcl"; import {BuffaloZcl} from "../../../src/zspec/zcl/buffaloZcl"; import {uint16To8Array, uint32To8Array, uint56To8Array} from "../../utils/math"; /** Header with Global frame type */ const GLOBAL_HEADER = new Zcl.Header( { frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0, }, undefined, 123, Zcl.Foundation.read.ID, ); const GLOBAL_HEADER_BUFFER = Buffer.from([0, 123, Zcl.Foundation.read.ID]); /** Header with Global frame type and server to client direction */ const GLOBAL_RSP_HEADER = new Zcl.Header( { frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, reservedBits: 0, }, undefined, 78, Zcl.Foundation.readRsp.ID, ); const GLOBAL_RSP_HEADER_BUFFER = Buffer.from([8, 78, Zcl.Foundation.readRsp.ID]); /** Header with Global frame type with command report */ const GLOBAL_HEADER_REPORT = new Zcl.Header( { frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0, }, undefined, 123, Zcl.Foundation.report.ID, ); const GLOBAL_HEADER_REPORT_BUFFER = Buffer.from([0, 123, Zcl.Foundation.report.ID]); /** Header with Global frame type and server to client direction including condition-based parameters */ const GLOBAL_CONDITION_HEADER = new Zcl.Header( { frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, reservedBits: 0, }, undefined, 78, Zcl.Foundation.configReport.ID, ); const GLOBAL_CONDITION_HEADER_BUFFER = Buffer.from([8, 78, Zcl.Foundation.configReport.ID]); /** Header with Specific frame type */ const SPECIFIC_HEADER = new Zcl.Header( { frameType: Zcl.FrameType.SPECIFIC, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0, }, undefined, 44, Zcl.Clusters.genAlarms.commands.getAlarm.ID, ); const SPECIFIC_HEADER_BUFFER = Buffer.from([1, 44, Zcl.Clusters.genAlarms.commands.getAlarm.ID]); /** Header with Specific frame type including condition-based parameters */ const SPECIFIC_CONDITION_HEADER = new Zcl.Header( { frameType: Zcl.FrameType.SPECIFIC, manufacturerSpecific: false, direction: Zcl.Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, reservedBits: 0, }, undefined, 45, Zcl.Clusters.genOta.commandsResponse.queryNextImageResponse.ID, ); const SPECIFIC_CONDITION_HEADER_BUFFER = Buffer.from([9, 45, Zcl.Clusters.genOta.commandsResponse.queryNextImageResponse.ID]); /** Header with Specific frame type and server to client direction */ const SPECIFIC_RSP_HEADER = new Zcl.Header( { frameType: Zcl.FrameType.SPECIFIC, manufacturerSpecific: false, direction: Zcl.Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, reservedBits: 0, }, undefined, 53, Zcl.Clusters.genAlarms.commandsResponse.alarm.ID, ); const SPECIFIC_RSP_HEADER_BUFFER = Buffer.from([9, 53, Zcl.Clusters.genAlarms.commandsResponse.alarm.ID]); /** Header with manufacturer-specific */ const MANUF_SPE_HEADER = new Zcl.Header( { frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: true, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0, }, Zcl.ManufacturerCode.AAC_TECHNOLOGIES_HOLDING, 234, Zcl.Foundation.read.ID, ); const MANUF_SPE_HEADER_BUFFER = Buffer.from([4, ...uint16To8Array(Zcl.ManufacturerCode.AAC_TECHNOLOGIES_HOLDING), 234, Zcl.Foundation.read.ID]); /** Frame of Global type */ const GLOBAL_FRAME = Zcl.Frame.create( GLOBAL_HEADER.frameControl.frameType, GLOBAL_HEADER.frameControl.direction, GLOBAL_HEADER.frameControl.disableDefaultResponse, GLOBAL_HEADER.manufacturerCode, GLOBAL_HEADER.transactionSequenceNumber, GLOBAL_HEADER.commandIdentifier, Zcl.Clusters.genBasic.ID, [{attrId: 256}] /*payload*/, {} /*custom clusters*/, GLOBAL_HEADER.frameControl.reservedBits, ); const GLOBAL_FRAME_BUFFER = Buffer.concat([GLOBAL_HEADER_BUFFER, Buffer.from(uint16To8Array(256))]); const GLOBAL_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":0,"disableDefaultResponse":false,"manufacturerSpecific":false},"transactionSequenceNumber":123,"commandIdentifier":0},"payload":[{"attrId":256}],"command":{"name":"read","ID":0,"response":1}}`; /** Frame of Global type with BigInt */ const GLOBAL_FRAME_BIG_INT = Zcl.Frame.create( GLOBAL_HEADER_REPORT.frameControl.frameType, GLOBAL_HEADER_REPORT.frameControl.direction, GLOBAL_HEADER_REPORT.frameControl.disableDefaultResponse, GLOBAL_HEADER_REPORT.manufacturerCode, GLOBAL_HEADER_REPORT.transactionSequenceNumber, GLOBAL_HEADER_REPORT.commandIdentifier, Zcl.Clusters.haApplianceIdentification.ID, [{attrId: 0, dataType: Zcl.DataType.UINT56, attrData: 200n}] /*payload*/, {} /*custom clusters*/, GLOBAL_HEADER_REPORT.frameControl.reservedBits, ); const GLOBAL_FRAME_BIG_INT_BUFFER = Buffer.concat([ GLOBAL_HEADER_REPORT_BUFFER, Buffer.from(uint16To8Array(0)), Buffer.from([Zcl.DataType.UINT56]), Buffer.from(uint56To8Array(200n)), ]); const GLOBAL_FRAME_BIG_INT_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":0,"disableDefaultResponse":false,"manufacturerSpecific":false},"transactionSequenceNumber":123,"commandIdentifier":10},"payload":[{"attrId":0,"dataType":38,"attrData":"200"}],"command":{"name":"report","ID":10}}`; /** Frame of Global type and response command */ const GLOBAL_RSP_FRAME = Zcl.Frame.create( GLOBAL_RSP_HEADER.frameControl.frameType, GLOBAL_RSP_HEADER.frameControl.direction, GLOBAL_RSP_HEADER.frameControl.disableDefaultResponse, GLOBAL_RSP_HEADER.manufacturerCode, GLOBAL_RSP_HEADER.transactionSequenceNumber, GLOBAL_RSP_HEADER.commandIdentifier, Zcl.Clusters.genPowerCfg.ID, [{attrId: 256, status: Zcl.Status.SUCCESS, dataType: Zcl.DataType.ENUM8, attrData: 127}] /*payload*/, {} /*custom clusters*/, GLOBAL_RSP_HEADER.frameControl.reservedBits, ); const GLOBAL_RSP_FRAME_BUFFER = Buffer.concat([ GLOBAL_RSP_HEADER_BUFFER, Buffer.from([...uint16To8Array(256), Zcl.Status.SUCCESS, Zcl.DataType.ENUM8, 127]), ]); const GLOBAL_RSP_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":1,"disableDefaultResponse":false,"manufacturerSpecific":false},"transactionSequenceNumber":78,"commandIdentifier":1},"payload":[{"attrId":256,"status":0,"dataType":48,"attrData":127}],"command":{"name":"readRsp","ID":1}}`; /** Frame of Global type with condition-based parameters */ const GLOBAL_CONDITION_FRAME = Zcl.Frame.create( GLOBAL_CONDITION_HEADER.frameControl.frameType, GLOBAL_CONDITION_HEADER.frameControl.direction, GLOBAL_CONDITION_HEADER.frameControl.disableDefaultResponse, GLOBAL_CONDITION_HEADER.manufacturerCode, GLOBAL_CONDITION_HEADER.transactionSequenceNumber, GLOBAL_CONDITION_HEADER.commandIdentifier, Zcl.Clusters.genOnOff.ID, [{direction: Zcl.Direction.SERVER_TO_CLIENT, attrId: 256, timeout: 10000}] /*payload*/, {} /*custom clusters*/, GLOBAL_CONDITION_HEADER.frameControl.reservedBits, ); const GLOBAL_CONDITION_FRAME_BUFFER = Buffer.concat([ GLOBAL_CONDITION_HEADER_BUFFER, Buffer.from([Zcl.Direction.SERVER_TO_CLIENT, ...uint16To8Array(256), ...uint16To8Array(10000)]), ]); const GLOBAL_CONDITION_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":1,"disableDefaultResponse":false,"manufacturerSpecific":false},"transactionSequenceNumber":78,"commandIdentifier":6},"payload":[{"direction":1,"attrId":256,"timeout":10000}],"command":{"name":"configReport","ID":6,"response":7}}`; /** Frame of Specific type */ const SPECIFIC_FRAME = Zcl.Frame.create( SPECIFIC_HEADER.frameControl.frameType, SPECIFIC_HEADER.frameControl.direction, SPECIFIC_HEADER.frameControl.disableDefaultResponse, SPECIFIC_HEADER.manufacturerCode, SPECIFIC_HEADER.transactionSequenceNumber, SPECIFIC_HEADER.commandIdentifier, Zcl.Clusters.genAlarms.ID, {} /*payload*/, {} /*custom clusters*/, SPECIFIC_HEADER.frameControl.reservedBits, ); const SPECIFIC_FRAME_BUFFER = Buffer.concat([SPECIFIC_HEADER_BUFFER]); const SPECIFIC_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":1,"direction":0,"disableDefaultResponse":false,"manufacturerSpecific":false},"transactionSequenceNumber":44,"commandIdentifier":2},"payload":{},"command":{"name":"getAlarm","ID":2,"parameters":[]}}`; /** Frame of Specific type and response command */ const SPECIFIC_RSP_FRAME = Zcl.Frame.create( SPECIFIC_RSP_HEADER.frameControl.frameType, SPECIFIC_RSP_HEADER.frameControl.direction, SPECIFIC_RSP_HEADER.frameControl.disableDefaultResponse, SPECIFIC_RSP_HEADER.manufacturerCode, SPECIFIC_RSP_HEADER.transactionSequenceNumber, SPECIFIC_RSP_HEADER.commandIdentifier, Zcl.Clusters.genAlarms.ID, {alarmcode: 246, clusterid: 3456} /*payload*/, {} /*custom clusters*/, SPECIFIC_RSP_HEADER.frameControl.reservedBits, ); const SPECIFIC_RSP_FRAME_BUFFER = Buffer.concat([SPECIFIC_RSP_HEADER_BUFFER, Buffer.from([246, ...uint16To8Array(3456)])]); const SPECIFIC_RSP_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":1,"direction":1,"disableDefaultResponse":false,"manufacturerSpecific":false},"transactionSequenceNumber":53,"commandIdentifier":0},"payload":{"alarmcode":246,"clusterid":3456},"command":{"name":"alarm","ID":0,"parameters":[{"name":"alarmcode","type":48},{"name":"clusterid","type":232}],"required":true}}`; /** Frame of Specific type with condition-based parameters */ const SPECIFIC_CONDITION_FRAME = Zcl.Frame.create( SPECIFIC_CONDITION_HEADER.frameControl.frameType, SPECIFIC_CONDITION_HEADER.frameControl.direction, SPECIFIC_CONDITION_HEADER.frameControl.disableDefaultResponse, SPECIFIC_CONDITION_HEADER.manufacturerCode, SPECIFIC_CONDITION_HEADER.transactionSequenceNumber, SPECIFIC_CONDITION_HEADER.commandIdentifier, Zcl.Clusters.genOta.ID, {status: Zcl.Status.ABORT} /*payload*/, {} /*custom clusters*/, SPECIFIC_CONDITION_HEADER.frameControl.reservedBits, ); const SPECIFIC_CONDITION_FRAME_BUFFER = Buffer.concat([SPECIFIC_CONDITION_HEADER_BUFFER, Buffer.from([149])]); const SPECIFIC_CONDITION_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":1,"direction":1,"disableDefaultResponse":false,"manufacturerSpecific":false},"transactionSequenceNumber":45,"commandIdentifier":2},"payload":{"status":149},"command":{"name":"queryNextImageResponse","ID":2,"parameters":[{"name":"status","type":48},{"name":"manufacturerCode","type":33,"conditions":[{"type":"fieldEquals","field":"status","value":0}]},{"name":"imageType","type":33,"conditions":[{"type":"fieldEquals","field":"status","value":0}],"max":65471},{"name":"fileVersion","type":35,"conditions":[{"type":"fieldEquals","field":"status","value":0}]},{"name":"imageSize","type":35,"conditions":[{"type":"fieldEquals","field":"status","value":0}]}],"required":true}}`; /** Frame manufacturer-specific */ const MANUF_SPE_FRAME = Zcl.Frame.create( MANUF_SPE_HEADER.frameControl.frameType, MANUF_SPE_HEADER.frameControl.direction, MANUF_SPE_HEADER.frameControl.disableDefaultResponse, MANUF_SPE_HEADER.manufacturerCode, MANUF_SPE_HEADER.transactionSequenceNumber, MANUF_SPE_HEADER.commandIdentifier, Zcl.Foundation.read.ID, [{attrId: 256}] /*payload*/, {} /*custom clusters*/, MANUF_SPE_HEADER.frameControl.reservedBits, ); const MANUF_SPE_FRAME_BUFFER = Buffer.concat([MANUF_SPE_HEADER_BUFFER, Buffer.from(uint16To8Array(256))]); const MANUF_SPE_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":0,"disableDefaultResponse":false,"manufacturerSpecific":true},"manufacturerCode":4344,"transactionSequenceNumber":234,"commandIdentifier":0},"payload":[{"attrId":256}],"command":{"name":"read","ID":0,"response":1}}`; const LIST_HEADER = new Zcl.Header( { frameType: Zcl.FrameType.SPECIFIC, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0, }, undefined, 43, Zcl.Clusters.genGroups.commands.getMembership.ID, ); const LIST_BUFFER = Buffer.from([1, 43, Zcl.Clusters.genGroups.commands.getMembership.ID, 2, 10, 0, 20, 0]); const LIST_FRAME = Zcl.Frame.create( LIST_HEADER.frameControl.frameType, LIST_HEADER.frameControl.direction, LIST_HEADER.frameControl.disableDefaultResponse, LIST_HEADER.manufacturerCode, LIST_HEADER.transactionSequenceNumber, LIST_HEADER.commandIdentifier, Zcl.Clusters.genGroups.ID, {groupcount: 2, grouplist: [10, 20]} /*payload*/, {} /*custom clusters*/, LIST_HEADER.frameControl.reservedBits, ); const LIST_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":1,"direction":0,"disableDefaultResponse":false,"manufacturerSpecific":false},"transactionSequenceNumber":43,"commandIdentifier":2},"payload":{"groupcount":2,"grouplist":[10,20]},"command":{"name":"getMembership","ID":2,"response":2,"parameters":[{"name":"groupcount","type":32},{"name":"grouplist","type":1002}],"required":true}}`; describe("ZCL Frame", () => { describe("Validates Parameter Condition", () => { it("MINIMUM_REMAINING_BUFFER_BYTES", () => { expect( Zcl.Frame.conditionsValid( { name: "direction", type: Zcl.DataType.DATA8, conditions: [{type: Zcl.ParameterCondition.MINIMUM_REMAINING_BUFFER_BYTES, value: 3}], }, {status: 1}, 3, ), ).toBeTruthy(); expect( Zcl.Frame.conditionsValid( { name: "direction", type: Zcl.DataType.DATA8, conditions: [{type: Zcl.ParameterCondition.MINIMUM_REMAINING_BUFFER_BYTES, value: 3}], }, {status: 1}, 2, ), ).toBeFalsy(); }); it("BITMASK_SET", () => { expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[8], {options: 0x4000}, undefined)).toBeTruthy(); expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[8], {options: 0x4150}, undefined)).toBeTruthy(); expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[8], {options: 0x0400}, undefined)).toBeFalsy(); expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[8], {options: 0x1400}, undefined)).toBeFalsy(); }); it("BITFIELD_ENUM", () => { // {param:'options', offset: 0, size: 3, value: 0b000} expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b000}, undefined)).toBeTruthy(); expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b1000}, undefined)).toBeTruthy(); expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b001}, undefined)).toBeFalsy(); expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b011}, undefined)).toBeFalsy(); expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b100}, undefined)).toBeFalsy(); expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b1010}, undefined)).toBeFalsy(); }); it("FIELD_EQUAL", () => { expect( Zcl.Frame.conditionsValid( { name: "dataType", type: Zcl.DataType.DATA8, conditions: [{type: Zcl.ParameterCondition.FIELD_EQUAL, field: "status", value: Zcl.Status.SUCCESS}], }, {status: 0}, undefined, ), ).toBeTruthy(); expect( Zcl.Frame.conditionsValid( { name: "dataType", type: Zcl.DataType.DATA8, conditions: [{type: Zcl.ParameterCondition.FIELD_EQUAL, field: "status", value: Zcl.Status.SUCCESS}], }, {status: 1}, undefined, ), ).toBeFalsy(); expect( Zcl.Frame.conditionsValid( { name: "dataType", type: Zcl.DataType.DATA8, conditions: [{type: Zcl.ParameterCondition.FIELD_EQUAL, field: "status", value: Zcl.Status.SUCCESS, reversed: true}], }, {status: 0}, undefined, ), ).toBeFalsy(); expect( Zcl.Frame.conditionsValid( { name: "dataType", type: Zcl.DataType.DATA8, conditions: [{type: Zcl.ParameterCondition.FIELD_EQUAL, field: "status", value: Zcl.Status.SUCCESS, reversed: true}], }, {status: 1}, undefined, ), ).toBeTruthy(); expect( Zcl.Frame.conditionsValid(Zcl.Clusters.touchlink.commandsResponse.scanResponse.parameters[13], {numberOfSubDevices: 1}, undefined), ).toBeTruthy(); expect( Zcl.Frame.conditionsValid(Zcl.Clusters.touchlink.commandsResponse.scanResponse.parameters[13], {numberOfSubDevices: 0}, undefined), ).toBeFalsy(); expect( Zcl.Frame.conditionsValid(Zcl.Clusters.touchlink.commandsResponse.scanResponse.parameters[13], {numberOfSubDevices: 3}, undefined), ).toBeFalsy(); expect( Zcl.Frame.conditionsValid(Zcl.Clusters.genOta.commandsResponse.imageNotify.parameters[2], {payloadType: 0}, undefined), ).toBeFalsy(); expect( Zcl.Frame.conditionsValid(Zcl.Clusters.genOta.commandsResponse.imageNotify.parameters[2], {payloadType: 1}, undefined), ).toBeTruthy(); expect( Zcl.Frame.conditionsValid(Zcl.Clusters.genOta.commandsResponse.imageNotify.parameters[3], {payloadType: 1}, undefined), ).toBeFalsy(); expect( Zcl.Frame.conditionsValid(Zcl.Clusters.genOta.commandsResponse.imageNotify.parameters[3], {payloadType: 2}, undefined), ).toBeTruthy(); expect( Zcl.Frame.conditionsValid(Zcl.Clusters.genOta.commandsResponse.imageNotify.parameters[4], {payloadType: 2}, undefined), ).toBeFalsy(); expect( Zcl.Frame.conditionsValid(Zcl.Clusters.genOta.commandsResponse.imageNotify.parameters[4], {payloadType: 3}, undefined), ).toBeTruthy(); }); }); describe("Header", () => { it.each([ [ "global", { frameControl: GLOBAL_HEADER.frameControl, manufacturerCode: GLOBAL_HEADER.manufacturerCode, transactionSequenceNumber: GLOBAL_HEADER.transactionSequenceNumber, commandId: GLOBAL_HEADER.commandIdentifier, }, {written: GLOBAL_HEADER_BUFFER}, ], [ "global response", { frameControl: GLOBAL_RSP_HEADER.frameControl, manufacturerCode: GLOBAL_RSP_HEADER.manufacturerCode, transactionSequenceNumber: GLOBAL_RSP_HEADER.transactionSequenceNumber, commandId: GLOBAL_RSP_HEADER.commandIdentifier, }, {written: GLOBAL_RSP_HEADER_BUFFER}, ], [ "specific", { frameControl: SPECIFIC_HEADER.frameControl, manufacturerCode: SPECIFIC_HEADER.manufacturerCode, transactionSequenceNumber: SPECIFIC_HEADER.transactionSequenceNumber, commandId: SPECIFIC_HEADER.commandIdentifier, }, {written: SPECIFIC_HEADER_BUFFER}, ], [ "manufacturer-specific", { frameControl: MANUF_SPE_HEADER.frameControl, manufacturerCode: MANUF_SPE_HEADER.manufacturerCode, transactionSequenceNumber: MANUF_SPE_HEADER.transactionSequenceNumber, commandId: MANUF_SPE_HEADER.commandIdentifier, }, {written: MANUF_SPE_HEADER_BUFFER}, ], [ "disable default response", { frameControl: { frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: true, reservedBits: 0, }, manufacturerCode: undefined, transactionSequenceNumber: 234, commandId: 1, }, {written: Buffer.from([16, 234, 1])}, ], [ "reserved bits - non-compliant use", { frameControl: { frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 7, }, manufacturerCode: undefined, transactionSequenceNumber: 234, commandId: 1, }, {written: Buffer.from([224, 234, 1])}, ], [ "specific manufacturer-specific", { frameControl: { frameType: Zcl.FrameType.SPECIFIC, manufacturerSpecific: true, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0, }, manufacturerCode: Zcl.ManufacturerCode.S3C, transactionSequenceNumber: 234, commandId: 1, }, {written: Buffer.from([5, ...uint16To8Array(Zcl.ManufacturerCode.S3C), 234, 1])}, ], [ "all non-zero - non-compliant use of reservedBits", { frameControl: { frameType: Zcl.FrameType.SPECIFIC, manufacturerSpecific: true, direction: Zcl.Direction.SERVER_TO_CLIENT, disableDefaultResponse: true, reservedBits: 3, }, manufacturerCode: Zcl.ManufacturerCode.BOSCH_SECURITY_SYSTEMS_INC, transactionSequenceNumber: 234, commandId: 1, }, {written: Buffer.from([125, ...uint16To8Array(Zcl.ManufacturerCode.BOSCH_SECURITY_SYSTEMS_INC), 234, 1])}, ], ])("Reads & Writes %s", (_name, payload, expected) => { // write { const header = new Zcl.Header(payload.frameControl, payload.manufacturerCode, payload.transactionSequenceNumber, payload.commandId); const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); header.write(buffalo); expect(buffer.subarray(0, header.length)).toStrictEqual(expected.written); expect(header.length).toStrictEqual(expected.written.length); expect(header.isGlobal).toStrictEqual(payload.frameControl.frameType === Zcl.FrameType.GLOBAL); expect(header.isSpecific).toStrictEqual(payload.frameControl.frameType === Zcl.FrameType.SPECIFIC); } // read { const header = Zcl.Header.fromBuffer(expected.written)!; expect(header).toBeInstanceOf(Zcl.Header); expect(header.length).toStrictEqual(expected.written.length); expect(header.isGlobal).toStrictEqual(payload.frameControl.frameType === Zcl.FrameType.GLOBAL); expect(header.isSpecific).toStrictEqual(payload.frameControl.frameType === Zcl.FrameType.SPECIFIC); expect(header.frameControl).toStrictEqual(payload.frameControl); expect(header.manufacturerCode).toStrictEqual(payload.manufacturerCode); expect(header.transactionSequenceNumber).toStrictEqual(payload.transactionSequenceNumber); expect(header.commandIdentifier).toStrictEqual(payload.commandId); } }); it.each([ ["basic", {value: [0, 234]}], ["manufacturer specific", {value: [4, ...uint16To8Array(1234), 234]}], ])("Reads non-compliant header as undefined %s", (_name, payload) => { expect(Zcl.Header.fromBuffer(Buffer.from(payload.value))).toStrictEqual(undefined); }); }); it.each([ ["global", GLOBAL_FRAME, {string: GLOBAL_FRAME_STRING, header: GLOBAL_HEADER, written: GLOBAL_FRAME_BUFFER}], [ "global BigInt", GLOBAL_FRAME_BIG_INT, {string: GLOBAL_FRAME_BIG_INT_STRING, header: GLOBAL_HEADER_REPORT, written: GLOBAL_FRAME_BIG_INT_BUFFER}, ], ["global response", GLOBAL_RSP_FRAME, {string: GLOBAL_RSP_FRAME_STRING, header: GLOBAL_RSP_HEADER, written: GLOBAL_RSP_FRAME_BUFFER}], [ "global with condition-based parameters", GLOBAL_CONDITION_FRAME, {string: GLOBAL_CONDITION_FRAME_STRING, header: GLOBAL_CONDITION_HEADER, written: GLOBAL_CONDITION_FRAME_BUFFER}, ], ["specific", SPECIFIC_FRAME, {string: SPECIFIC_FRAME_STRING, header: SPECIFIC_HEADER, written: SPECIFIC_FRAME_BUFFER}], [ "specific response", SPECIFIC_RSP_FRAME, {string: SPECIFIC_RSP_FRAME_STRING, header: SPECIFIC_RSP_HEADER, written: SPECIFIC_RSP_FRAME_BUFFER}, ], [ "specific with condition-based parameters", SPECIFIC_CONDITION_FRAME, {string: SPECIFIC_CONDITION_FRAME_STRING, header: SPECIFIC_CONDITION_HEADER, written: SPECIFIC_CONDITION_FRAME_BUFFER}, ], ["manufacturer-specific", MANUF_SPE_FRAME, {string: MANUF_SPE_FRAME_STRING, header: MANUF_SPE_HEADER, written: MANUF_SPE_FRAME_BUFFER}], ["list", LIST_FRAME, {string: LIST_STRING, header: LIST_HEADER, written: LIST_BUFFER}], ])("Writes & Reads frame %s", (_name, frame, expected) => { expect(frame).toBeDefined(); expect(frame.toString()).toStrictEqual(expected.string); expect(frame.header).toStrictEqual(expected.header); expect(frame.toBuffer()).toStrictEqual(expected.written); expect(Zcl.Frame.fromBuffer(frame.cluster.ID, frame.header, expected.written, {}).toString()).toStrictEqual(expected.string); }); it("Writes & Reads repetitive strategy", () => { const expected = [{attrId: 256}, {attrId: 127}]; const header = new Zcl.Header( { frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0, }, undefined, 123, Zcl.Foundation.read.ID, ); const frame = Zcl.Frame.create( header.frameControl.frameType, header.frameControl.direction, header.frameControl.disableDefaultResponse, header.manufacturerCode, header.transactionSequenceNumber, header.commandIdentifier, Zcl.Foundation.read.ID, expected /*payload*/, {} /*custom clusters*/, header.frameControl.reservedBits, ); expect(frame.payload).toStrictEqual(expected); const buffer = frame.toBuffer(); const readHeader = Zcl.Header.fromBuffer(buffer)!; const readFrame = Zcl.Frame.fromBuffer(0 /*Foundation*/, readHeader, buffer, {} /*custom clusters*/); expect(readFrame.payload).toStrictEqual(expected); }); it("Writes & Reads flat strategy", () => { const expected = {cmdId: 253, statusCode: 127}; const header = new Zcl.Header( { frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0, }, undefined, 123, Zcl.Foundation.defaultRsp.ID, ); const frame = Zcl.Frame.create( header.frameControl.frameType, header.frameControl.direction, header.frameControl.disableDefaultResponse, header.manufacturerCode, header.transactionSequenceNumber, header.commandIdentifier, Zcl.Foundation.defaultRsp.ID, expected /*payload*/, {} /*custom clusters*/, header.frameControl.reservedBits, ); expect(frame.payload).toStrictEqual(expected); const buffer = frame.toBuffer(); const readHeader = Zcl.Header.fromBuffer(buffer)!; const readFrame = Zcl.Frame.fromBuffer(0 /*Foundation*/, readHeader, buffer, {} /*custom clusters*/); expect(readFrame.payload).toStrictEqual(expected); }); it("Writes & Reads oneof strategy", () => { const expected = { discComplete: 123, attrInfos: [ {attrId: 32, dataType: Zcl.DataType.UINT16}, {attrId: 67, dataType: Zcl.DataType.ARRAY}, ], }; const header = new Zcl.Header( { frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0, }, undefined, 123, Zcl.Foundation.discoverRsp.ID, ); const frame = Zcl.Frame.create( header.frameControl.frameType, header.frameControl.direction, header.frameControl.disableDefaultResponse, header.manufacturerCode, header.transactionSequenceNumber, header.commandIdentifier, Zcl.Foundation.discoverRsp.ID, expected /*payload*/, {} /*custom clusters*/, header.frameControl.reservedBits, ); expect(frame.payload).toStrictEqual(expected); const buffer = frame.toBuffer(); const readHeader = Zcl.Header.fromBuffer(buffer)!; const readFrame = Zcl.Frame.fromBuffer(0 /*Foundation*/, readHeader, buffer, {} /*custom clusters*/); expect(readFrame.payload).toStrictEqual(expected); }); it("Throws when writting invalid frame type", () => { expect(() => { const frame = Zcl.Frame.create( // @ts-expect-error invalid on purpose 3, Zcl.Direction.CLIENT_TO_SERVER, false, null, 123, Zcl.Clusters.genAlarms.commands.reset.ID, Zcl.Clusters.genAlarms.ID, {} /*payload*/, {} /*custom clusters*/, 0 /*reserved bits*/, ); frame.toBuffer(); }).toThrow(`Frametype '3' not valid`); }); it("Throws when reading invalid frame type", () => { expect(() => { Zcl.Frame.fromBuffer( Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(Buffer.from([2, 123, 0]))!, Buffer.from([]) /*payload*/, {} /*custom clusters*/, ); }).toThrow(`Unsupported frameType '2'`); }); it("Throws when reading frame without header", () => { expect(() => { Zcl.Frame.fromBuffer( Zcl.Clusters.genBasic.ID, // @ts-expect-error invalid of purpose null, Buffer.from([]) /*payload*/, {} /*custom clusters*/, ); }).toThrow("Invalid ZclHeader."); }); it("Throws when writting missing payload", () => { expect(() => { const frame = Zcl.Frame.create( SPECIFIC_RSP_HEADER.frameControl.frameType, SPECIFIC_RSP_HEADER.frameControl.direction, SPECIFIC_RSP_HEADER.frameControl.disableDefaultResponse, SPECIFIC_RSP_HEADER.manufacturerCode, SPECIFIC_RSP_HEADER.transactionSequenceNumber, SPECIFIC_RSP_HEADER.commandIdentifier, Zcl.Clusters.genAlarms.ID, {alarmcode: 246 /*clusterid: 3456*/} /*payload*/, {} /*custom clusters*/, SPECIFIC_RSP_HEADER.frameControl.reservedBits, ); frame.toBuffer(); }).toThrow(`Parameter 'clusterid' is missing`); }); it("Checks cluster match by name", () => { expect(GLOBAL_FRAME.isCluster("genBasic")).toBeTruthy(); expect(GLOBAL_FRAME.isCluster("discoverCommands")).toBeFalsy(); // @ts-expect-error invalid on purpose expect(GLOBAL_FRAME.isCluster("notacluster")).toBeFalsy(); expect(SPECIFIC_FRAME.isCluster("genAlarms")).toBeTruthy(); expect(SPECIFIC_FRAME.isCluster("genAnalogInput")).toBeFalsy(); // @ts-expect-error invalid on purpose expect(SPECIFIC_FRAME.isCluster("notacluster")).toBeFalsy(); }); it("Checks command match by name", () => { expect(GLOBAL_FRAME.isCommand("read")).toBeTruthy(); expect(GLOBAL_FRAME.isCommand("discoverCommands")).toBeFalsy(); // @ts-expect-error invalid on purpose expect(GLOBAL_FRAME.isCommand("notacommand")).toBeFalsy(); expect(SPECIFIC_FRAME.isCommand("getAlarm")).toBeTruthy(); expect(SPECIFIC_FRAME.isCommand("enrollReq")).toBeFalsy(); // @ts-expect-error invalid on purpose expect(SPECIFIC_FRAME.isCommand("notacommand")).toBeFalsy(); }); it("[workaround] Reads Foundation char str as Mi struct for Xiaomi attridId=65281", () => { const expected = [ {attrId: 5, dataType: Zcl.DataType.CHAR_STR, attrData: "lumi.sensor_wleak.aq1"}, {attrId: 65281, dataType: Zcl.DataType.CHAR_STR, attrData: {1: 3285, 3: 33, 4: 5032, 5: 43, 6: 327680, 8: 516, 10: 0, 100: 0}}, ]; const buffer = Buffer.from([ 28, ...uint16To8Array(Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN), 3, 10 /*header*/, // regular attr parsing 5, 0, Zcl.DataType.CHAR_STR, 21, 108, 117, 109, 105, 46, 115, 101, 110, 115, 111, 114, 95, 119, 108, 101, 97, 107, 46, 97, 113, 49, // workaround parsing 1, 255, Zcl.DataType.CHAR_STR, 34, 1, Zcl.DataType.UINT16, ...uint16To8Array(3285), 3, Zcl.DataType.INT8, 33, 4, Zcl.DataType.UINT16, ...uint16To8Array(5032), 5, Zcl.DataType.UINT16, ...uint16To8Array(43), 6, Zcl.DataType.UINT40, ...uint32To8Array(327680), 0, 8, Zcl.DataType.UINT16, ...uint16To8Array(516), 10, Zcl.DataType.UINT16, ...uint16To8Array(0), 100, Zcl.DataType.BOOLEAN, 0, ]); const header = Zcl.Header.fromBuffer(buffer)!; const frame = Zcl.Frame.fromBuffer(0 /*Foundation*/, header, buffer, {}); expect(frame.payload).toStrictEqual(expected); }); it("[workaround] Reads Foundation struct with extra payload to match zcl-packet", () => { const expected = [ { attrId: 65282, dataType: Zcl.DataType.STRUCT, attrData: [ {elmType: Zcl.DataType.BOOLEAN, elmVal: 1}, {elmType: Zcl.DataType.UINT16, elmVal: 3022}, {elmType: Zcl.DataType.UINT16, elmVal: 17320}, {elmType: Zcl.DataType.UINT40, elmVal: 1}, {elmType: Zcl.DataType.UINT16, elmVal: 560}, {elmType: Zcl.DataType.UINT8, elmVal: 86}, ], }, ]; const buffer = Buffer.from([ 28, ...uint16To8Array(Zcl.ManufacturerCode.DSR_CORPORATION), 194, 10 /*header*/, ...uint16To8Array(65282), Zcl.DataType.STRUCT, 6, 0, Zcl.DataType.BOOLEAN, 1, Zcl.DataType.UINT16, ...uint16To8Array(3022), Zcl.DataType.UINT16, ...uint16To8Array(17320), Zcl.DataType.UINT40, ...uint32To8Array(1), 0, Zcl.DataType.UINT16, ...uint16To8Array(560), Zcl.DataType.UINT8, 86, ]); const header = Zcl.Header.fromBuffer(buffer)!; const frame = Zcl.Frame.fromBuffer(0 /*Foundation*/, header, buffer, {}); expect(frame.payload).toStrictEqual(expected); }); it("Reads/writes Touchlink Scan Response with different size payloads", () => { const full = Buffer.from([ 0x19, 0x0, 0x1, 0xe0, 0xde, 0x5e, 0x2f, 0xa, 0x5, 0x0, 0x12, 0x0, 0x3d, 0x30, 0x1d, 0x4a, 0x8f, 0xb7, 0xdc, 0x1c, 0x0, 0x4b, 0x12, 0x0, 0x0, 0xf, 0x62, 0x1a, 0xb3, 0xaa, 0x1, 0x0, 0xb, 0x5e, 0xc0, 0x10, 0x2, 0x2, 0x0, ]); const short = Buffer.from([ 0x19, 0x0, 0x1, 0xe0, 0xde, 0x5e, 0x2f, 0xa, 0x5, 0x0, 0x12, 0x0, 0x3d, 0x30, 0x1d, 0x4a, 0x8f, 0xb7, 0xdc, 0x1c, 0x0, 0x4b, 0x12, 0x0, 0x0, 0xf, 0x62, 0x1a, 0xb3, 0xaa, 0x0, 0x0, ]); const short2 = Buffer.from([ 0x19, 0x0, 0x1, 0xe0, 0xde, 0x5e, 0x2f, 0xa, 0x5, 0x0, 0x12, 0x0, 0x3d, 0x30, 0x1d, 0x4a, 0x8f, 0xb7, 0xdc, 0x1c, 0x0, 0x4b, 0x12, 0x0, 0x0, 0xf, 0x62, 0x1a, 0xb3, 0xaa, 0x3, 0x0, ]); const fullZcl = Zcl.Frame.fromBuffer(0x1000, Zcl.Header.fromBuffer(full), full, {}); expect(fullZcl.payload.numberOfSubDevices).toStrictEqual(1); expect(fullZcl.payload.endpointID).toStrictEqual(11); expect(fullZcl.payload.profileID).toStrictEqual(0xc05e); expect(fullZcl.payload.deviceID).toStrictEqual(0x0210); expect(fullZcl.payload.version).toStrictEqual(0x02); expect(fullZcl.payload.groupIDCount).toStrictEqual(0); const shortZcl = Zcl.Frame.fromBuffer(0x1000, Zcl.Header.fromBuffer(short), short, {}); expect(shortZcl.payload.numberOfSubDevices).toStrictEqual(0); expect(shortZcl.payload.endpointID).toStrictEqual(undefined); expect(shortZcl.payload.profileID).toStrictEqual(undefined); expect(shortZcl.payload.deviceID).toStrictEqual(undefined); expect(shortZcl.payload.version).toStrictEqual(undefined); expect(shortZcl.payload.groupIDCount).toStrictEqual(undefined); const short2Zcl = Zcl.Frame.fromBuffer(0x1000, Zcl.Header.fromBuffer(short2), short2, {}); expect(short2Zcl.payload.numberOfSubDevices).toStrictEqual(3); expect(short2Zcl.payload.endpointID).toStrictEqual(undefined); expect(short2Zcl.payload.profileID).toStrictEqual(undefined); expect(short2Zcl.payload.deviceID).toStrictEqual(undefined); expect(short2Zcl.payload.version).toStrictEqual(undefined); expect(short2Zcl.payload.groupIDCount).toStrictEqual(undefined); const fullNew = fullZcl.toBuffer(); expect(fullNew).toStrictEqual(full); const shortNew = shortZcl.toBuffer(); expect(shortNew).toStrictEqual(short); const short2New = short2Zcl.toBuffer(); expect(short2New).toStrictEqual(short2); }); it("throws when payload too short to continue reading", () => { const b = Buffer.from([1, 3, 3, 0, 33, 16, 18, 0, 255, 69, 81, 0, 62, 0, 0, 0]); expect(() => { Zcl.Frame.fromBuffer(Zcl.Clusters.genOta.ID, Zcl.Header.fromBuffer(b), b, {}); }).toThrow( `Status 'INVALID_FIELD' genOta:imageBlockRequest:maximumDataSize (The value of "offset" is out of range. It must be >= 0 and <= 15. Received 16)`, ); }); // https://github.com/Koenkk/zigbee2mqtt/issues/28217 it("[workaround] Reads genOta imageBlockRequest even if minimumBlockPeriod is missing", () => { const b = Buffer.from([1, 3, 3, 2, 33, 16, 18, 0, 255, 69, 81, 0, 62, 0, 0, 0, 8]); const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genOta.ID, Zcl.Header.fromBuffer(b), b, {}); expect(frame.payload).toStrictEqual({ fieldControl: 2, manufacturerCode: 4129, imageType: 18, fileVersion: 5326335, fileOffset: 62, maximumDataSize: 8, }); }); it("Keeps MINIMUM_REMAINING_BUFFER_BYTES parameters when present", () => { // moveToLevelWithOnOff with optionsMask/optionsOverride const payload = {level: 150, transtime: 0, optionsMask: 0, optionsOverride: 1}; const frame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.CLIENT_TO_SERVER, false, undefined, 1, "moveToLevelWithOnOff", Zcl.Clusters.genLevelCtrl.ID, payload, {}, ); const buffer = frame.toBuffer(); expect(buffer).toStrictEqual(Buffer.from([0x01, 0x01, 0x04, 0x96, 0x00, 0x00, 0x00, 0x01])); }); it("Allows omitting MINIMUM_REMAINING_BUFFER_BYTES parameters", () => { // moveToLevelWithOnOff without optionsMask/optionsOverride const payload = {level: 150, transtime: 0}; const frame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.CLIENT_TO_SERVER, false, undefined, 1, "moveToLevelWithOnOff", Zcl.Clusters.genLevelCtrl.ID, payload, {}, ); const buffer = frame.toBuffer(); expect(buffer).toStrictEqual(Buffer.from([0x01, 0x01, 0x04, 0x96, 0x00, 0x00])); }); it("Allows omitting parameters with false conditions", () => { // Touchlink scanResponse without sub-device fields when numberOfSubDevices != 1 const payload = { transactionID: 1, rssiCorrection: 10, zigbeeInformation: 5, touchlinkInformation: 3, keyBitmask: 15, responseID: 100, extendedPanID: "0x0011223344556677", networkUpdateID: 1, logicalChannel: 11, panID: 0x1234, networkAddress: 0x5678, numberOfSubDevices: 0, totalGroupIdentifiers: 0, }; const frame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, false, undefined, 2, "scanResponse", Zcl.Clusters.touchlink.ID, payload, {}, ); const buffer = frame.toBuffer(); expect(buffer).toStrictEqual( Buffer.from([ 0x09, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x05, 0x03, 0x0f, 0x00, 0x64, 0x00, 0x00, 0x00, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x01, 0x0b, 0x34, 0x12, 0x78, 0x56, 0x00, 0x00, ]), ); }); it("Requires parameters when conditions are true", () => { // queryNextImageResponse with status=SUCCESS requires manufacturerCode const payload = {status: 0}; // SUCCESS but missing required fields expect(() => { Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, false, undefined, 3, "queryNextImageResponse", Zcl.Clusters.genOta.ID, payload, {}, ).toBuffer(); }).toThrow("Parameter 'manufacturerCode' is missing"); }); });