UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

1,225 lines (1,103 loc) 50.2 kB
import * as Zcl from "../../../src/zspec/zcl"; import {BuffaloZcl} from "../../../src/zspec/zcl/buffaloZcl"; import {uint16To8Array, uint32To8Array} from "../../utils/math"; describe("ZCL Buffalo", () => { it("Writes invalida data type as buffer if buffer-like", () => { const value = [1, 2, 3]; const buffer = Buffer.alloc(3); const buffalo = new BuffaloZcl(buffer); const writeSpy = vi.spyOn(buffalo, "writeBuffer"); // @ts-expect-error invalid on purpose buffalo.write(99999, value, {}); expect(writeSpy).toHaveBeenCalledWith(value, value.length); expect(writeSpy).toHaveBeenCalledTimes(1); // @ts-expect-error private buffalo.position = 0; // @ts-expect-error invalid on purpose buffalo.write(99999, Buffer.from(value), {}); expect(writeSpy).toHaveBeenLastCalledWith(Buffer.from(value), value.length); expect(writeSpy).toHaveBeenCalledTimes(2); }); it("Throws when write/read invalid data type, except for write if buffer-like", () => { expect(() => { const buffer = Buffer.alloc(1); const buffalo = new BuffaloZcl(buffer); // @ts-expect-error invalid on purpose buffalo.write(99999, 127, {}); }).toThrow(); expect(() => { const buffer = Buffer.alloc(1); const buffalo = new BuffaloZcl(buffer); // @ts-expect-error invalid on purpose buffalo.read(99999, {}); }).toThrow(); }); it("Writes nothing", () => { for (const type of [Zcl.DataType.NO_DATA, Zcl.DataType.UNKNOWN]) { const buffer = Buffer.alloc(3); const buffalo = new BuffaloZcl(buffer); buffalo.write(type, 123, {}); expect(buffer).toStrictEqual(Buffer.from([0, 0, 0])); expect(buffalo.getPosition()).toStrictEqual(0); } { const buffer = Buffer.alloc(3); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, null, {}); expect(buffer).toStrictEqual(Buffer.from([0, 0, 0])); expect(buffalo.getPosition()).toStrictEqual(0); } }); it("Reads nothing", () => { for (const type of [Zcl.DataType.NO_DATA, Zcl.DataType.UNKNOWN]) { const buffer = Buffer.from([1, 2]); const buffalo = new BuffaloZcl(buffer); expect(buffalo.read(type, {})).toStrictEqual(undefined); expect(buffalo.getPosition()).toStrictEqual(0); } }); it.each([ [ "uint8-like", {value: 250, types: [Zcl.DataType.DATA8, Zcl.DataType.BOOLEAN, Zcl.DataType.BITMAP8, Zcl.DataType.UINT8, Zcl.DataType.ENUM8]}, {position: 1, write: "writeUInt8", read: "readUInt8"}, ], [ "uint16-like", { value: 65530, types: [ Zcl.DataType.DATA16, Zcl.DataType.BITMAP16, Zcl.DataType.UINT16, Zcl.DataType.ENUM16, Zcl.DataType.CLUSTER_ID, Zcl.DataType.ATTR_ID, ], }, {position: 2, write: "writeUInt16", read: "readUInt16"}, ], [ "uint24-like", {value: 16777210, types: [Zcl.DataType.DATA24, Zcl.DataType.BITMAP24, Zcl.DataType.UINT24]}, {position: 3, write: "writeUInt24", read: "readUInt24"}, ], [ "uint32-like", {value: 4294967290, types: [Zcl.DataType.DATA32, Zcl.DataType.BITMAP32, Zcl.DataType.UINT32, Zcl.DataType.UTC, Zcl.DataType.BAC_OID]}, {position: 4, write: "writeUInt32", read: "readUInt32"}, ], ["int8-like", {value: -120, types: [Zcl.DataType.INT8]}, {position: 1, write: "writeInt8", read: "readInt8"}], ["int16-like", {value: -32760, types: [Zcl.DataType.INT16]}, {position: 2, write: "writeInt16", read: "readInt16"}], ["int24-like", {value: -8388600, types: [Zcl.DataType.INT24]}, {position: 3, write: "writeInt24", read: "readInt24"}], ["int32-like", {value: -2147483640, types: [Zcl.DataType.INT32]}, {position: 4, write: "writeInt32", read: "readInt32"}], ["int48-like", {value: -140737488355320, types: [Zcl.DataType.INT48]}, {position: 6, write: "writeInt48", read: "readInt48"}], ["float-like", {value: 1.539989614439558e-36, types: [Zcl.DataType.SINGLE_PREC]}, {position: 4, write: "writeFloatLE", read: "readFloatLE"}], [ "double-like", {value: 5.447603722011605e-270, types: [Zcl.DataType.DOUBLE_PREC]}, {position: 8, write: "writeDoubleLE", read: "readDoubleLE"}, ], ["IEEE address", {value: "0xfe1234abcd9876ff", types: [Zcl.DataType.IEEE_ADDR]}, {position: 8, write: "writeIeeeAddr", read: "readIeeeAddr"}], [ "uint8-like list", {value: [250, 25, 50], types: [Zcl.BuffaloZclDataType.LIST_UINT8]}, {position: 3, write: "writeListUInt8", read: "readListUInt8"}, ], [ "uint16-like list", {value: [65530, 6553, 5530], types: [Zcl.BuffaloZclDataType.LIST_UINT16]}, {position: 6, write: "writeListUInt16", read: "readListUInt16"}, ], [ "uint24-like list", {value: [16777210, 1677721, 6777210], types: [Zcl.BuffaloZclDataType.LIST_UINT24]}, {position: 9, write: "writeListUInt24", read: "readListUInt24"}, ], [ "uint32-like list", {value: [4294967290, 429496729, 294967290], types: [Zcl.BuffaloZclDataType.LIST_UINT32]}, {position: 12, write: "writeListUInt32", read: "readListUInt32"}, ], [ "buffer", {value: Buffer.from([1, 2, 3, 4]), types: [Zcl.BuffaloZclDataType.BUFFER]}, {position: 4, write: "writeBuffer", read: "readBuffer"}, ], [ "security key", {value: Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]), types: [Zcl.DataType.SEC_KEY]}, {position: 16, write: "writeBuffer", read: "readBuffer"}, ], ])("Writes & Reads using base class for %s", (_name, payload, expected) => { const readOptions: {length?: number} = {}; if (Array.isArray(payload.value) || payload.value instanceof Buffer) { readOptions.length = payload.value.length; } for (const type of payload.types) { const buffer = Buffer.alloc(255); const buffalo = new BuffaloZcl(buffer); // @ts-expect-error dynamic const writeSpy = vi.spyOn(buffalo, expected.write); // @ts-expect-error dynamic const readSpy = vi.spyOn(buffalo, expected.read); buffalo.write(type, payload.value, {}); expect(writeSpy).toHaveBeenCalledTimes(1); expect(buffalo.getPosition()).toStrictEqual(expected.position); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(type, readOptions)).toStrictEqual(payload.value); expect(readSpy).toHaveBeenCalledTimes(1); expect(buffalo.getPosition()).toStrictEqual(expected.position); } }); it("Reads whole buffer without length option", () => { const value = [1, 2, 3, 4]; const buffer = Buffer.alloc(4); const buffalo = new BuffaloZcl(buffer); const writeSpy = vi.spyOn(buffalo, "writeBuffer"); const readSpy = vi.spyOn(buffalo, "readBuffer"); buffalo.write(Zcl.BuffaloZclDataType.BUFFER, Buffer.from(value), {}); expect(writeSpy).toHaveBeenCalledTimes(1); // XXX: inconsistent with read, write is always "whole" expect(writeSpy).toHaveBeenCalledWith(Buffer.from(value), value.length); expect(buffalo.getPosition()).toStrictEqual(value.length); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.BuffaloZclDataType.BUFFER, {})).toStrictEqual(Buffer.from(value)); expect(readSpy).toHaveBeenCalledTimes(1); expect(readSpy).toHaveBeenCalledWith(value.length); expect(buffalo.getPosition()).toStrictEqual(value.length); }); it("Reads partial buffer with length option", () => { const value = [1, 2, 3, 4]; const length = 2; const buffer = Buffer.alloc(255); const buffalo = new BuffaloZcl(buffer); const writeSpy = vi.spyOn(buffalo, "writeBuffer"); const readSpy = vi.spyOn(buffalo, "readBuffer"); buffalo.write(Zcl.BuffaloZclDataType.BUFFER, Buffer.from(value), {length}); expect(writeSpy).toHaveBeenCalledTimes(1); // XXX: inconsistent with read, write is always "whole" expect(writeSpy).toHaveBeenCalledWith(Buffer.from(value), value.length); expect(buffalo.getPosition()).toStrictEqual(value.length); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.BuffaloZclDataType.BUFFER, {length})).toStrictEqual(Buffer.from([value[0], value[1]])); expect(readSpy).toHaveBeenCalledTimes(1); expect(readSpy).toHaveBeenCalledWith(length); expect(buffalo.getPosition()).toStrictEqual(length); }); it("Writes & Reads octet str", () => { const value = [0xfe, 0x01, 0xab, 0x98]; const expectedPosition = 5; const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.DataType.OCTET_STR, value, {}); expect(buffalo.getPosition()).toStrictEqual(expectedPosition); expect(buffalo.getWritten()).toStrictEqual(Buffer.from([value.length, ...value])); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.DataType.OCTET_STR, {})).toStrictEqual(Buffer.from(value)); expect(buffalo.getPosition()).toStrictEqual(expectedPosition); }); it("Writes & Reads long octet str", () => { const value = [0xfe, 0x01, 0xab, 0x98]; const expectedPosition = 6; const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.DataType.LONG_OCTET_STR, value, {}); expect(buffalo.getPosition()).toStrictEqual(expectedPosition); expect(buffalo.getWritten()).toStrictEqual(Buffer.from([value.length, 0 /*length uint16*/, ...value])); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.DataType.LONG_OCTET_STR, {})).toStrictEqual(Buffer.from(value)); expect(buffalo.getPosition()).toStrictEqual(expectedPosition); }); it("Writes char str from number array", () => { const value = [0x61, 0x62, 0x63, 0x64]; const expectedPosition = 4; // value.length not written when number array given const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.DataType.CHAR_STR, value, {}); expect(buffalo.getPosition()).toStrictEqual(expectedPosition); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(value)); // see above comment }); it("Writes & Reads char str from string", () => { const value = "abcd"; const expectedValue = [value.length, 0x61, 0x62, 0x63, 0x64]; const expectedPosition = 5; const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.DataType.CHAR_STR, value, {}); expect(buffalo.getPosition()).toStrictEqual(expectedPosition); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedValue)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.DataType.CHAR_STR, {})).toStrictEqual(value); expect(buffalo.getPosition()).toStrictEqual(expectedPosition); }); it("[workaround] Reads char str as Mi struct for Xiaomi attridId=65281", () => { const expectedValue = {"1": 3285, "3": 33, "4": 5032, "5": 43, "6": 327680, "8": 516, "10": 0, "100": 0}; const buffer = Buffer.from([ 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 buffalo = new BuffaloZcl(buffer); expect(buffalo.read(Zcl.BuffaloZclDataType.MI_STRUCT, {})).toStrictEqual(expectedValue); expect(buffalo.getPosition()).toStrictEqual(buffer.length); }); it("Writes & Reads long char str", () => { const value = "abcd"; const expectedValue = [value.length, 0 /*length uint16*/, 0x61, 0x62, 0x63, 0x64]; const expectedPosition = 6; const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.DataType.LONG_CHAR_STR, value, {}); expect(buffalo.getPosition()).toStrictEqual(expectedPosition); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedValue)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.DataType.LONG_CHAR_STR, {})).toStrictEqual(value); expect(buffalo.getPosition()).toStrictEqual(expectedPosition); }); it.each([ [ "uint16", {value: {elementType: Zcl.DataType.UINT16, elements: [256, 1, 65530, 0]}}, { written: [ Zcl.DataType.UINT16, 4, 0 /*length uint16*/, ...uint16To8Array(256), ...uint16To8Array(1), ...uint16To8Array(65530), ...uint16To8Array(0), ], }, ], [ "char str", {value: {elementType: Zcl.DataType.CHAR_STR, elements: ["abcd", "cd", "a"]}}, {written: [Zcl.DataType.CHAR_STR, 3, 0 /*length uint16*/, 4, 0x61, 0x62, 0x63, 0x64, 2, 0x63, 0x64, 1, 0x61]}, ], [ "uint16 with element type passed as string key of Zcl.DataType", {value: {elementType: "UINT16", elements: [256, 1, 65530, 0]}}, { written: [ Zcl.DataType.UINT16, 4, 0 /*length uint16*/, ...uint16To8Array(256), ...uint16To8Array(1), ...uint16To8Array(65530), ...uint16To8Array(0), ], }, ], ])("Writes & Reads array of %s", (_name, payload, expected) => { const buffer = Buffer.alloc(50); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.DataType.ARRAY, payload.value, {}); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.DataType.ARRAY, {})).toStrictEqual(payload.value.elements); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); }); it.each([ [ "uint16", { value: [ {elmType: Zcl.DataType.UINT16, elmVal: 256}, {elmType: Zcl.DataType.UINT16, elmVal: 1}, {elmType: Zcl.DataType.UINT16, elmVal: 65530}, {elmType: Zcl.DataType.UINT16, elmVal: 0}, ], }, { written: [ 4, 0 /*length uint16*/, Zcl.DataType.UINT16, ...uint16To8Array(256), Zcl.DataType.UINT16, ...uint16To8Array(1), Zcl.DataType.UINT16, ...uint16To8Array(65530), Zcl.DataType.UINT16, ...uint16To8Array(0), ], }, ], [ "char str", { value: [ {elmType: Zcl.DataType.CHAR_STR, elmVal: "abcd"}, {elmType: Zcl.DataType.CHAR_STR, elmVal: "cd"}, {elmType: Zcl.DataType.CHAR_STR, elmVal: "a"}, ], }, { written: [ 3, 0 /*length uint16*/, Zcl.DataType.CHAR_STR, 4, 0x61, 0x62, 0x63, 0x64, Zcl.DataType.CHAR_STR, 2, 0x63, 0x64, Zcl.DataType.CHAR_STR, 1, 0x61, ], }, ], [ "mixed", { value: [ {elmType: Zcl.DataType.UINT16, elmVal: 256}, {elmType: Zcl.DataType.CHAR_STR, elmVal: "abcd"}, {elmType: Zcl.DataType.BITMAP8, elmVal: 3}, ], }, { written: [ 3, 0 /*length uint16*/, Zcl.DataType.UINT16, ...uint16To8Array(256), Zcl.DataType.CHAR_STR, 4, 0x61, 0x62, 0x63, 0x64, Zcl.DataType.BITMAP8, 3, ], }, ], ])("Writes & Reads struct of %s", (_name, payload, expected) => { const buffer = Buffer.alloc(50); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.DataType.STRUCT, payload.value, {}); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.DataType.STRUCT, {})).toStrictEqual(payload.value); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); }); it("Writes & Reads Time of Day", () => { const value = {hours: 0, minutes: 59, seconds: 34, hundredths: 88}; const expectedWritten = [0, 59, 34, 88]; const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.DataType.TOD, value, {}); expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedWritten)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.DataType.TOD, {})).toStrictEqual(value); expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); }); it("Writes & Reads Date", () => { const value = {year: 2000, month: 8, dayOfMonth: 31, dayOfWeek: 3}; const expectedWritten = [100, 8, 31, 3]; const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.DataType.DATE, value, {}); expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedWritten)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.DataType.DATE, {})).toStrictEqual(value); expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); }); it.each([ ["octet str", {type: Zcl.DataType.OCTET_STR, position: 1, returned: Buffer.from([])}], ["char str", {type: Zcl.DataType.CHAR_STR, position: 1, returned: ""}], ["long octet str", {type: Zcl.DataType.LONG_OCTET_STR, position: 2, returned: Buffer.from([])}], ["long char str", {type: Zcl.DataType.LONG_CHAR_STR, position: 2, returned: ""}], ["array", {type: Zcl.DataType.ARRAY, position: 3, returned: []}], ["struct", {type: Zcl.DataType.STRUCT, position: 2, returned: []}], [ "time of day", {type: Zcl.DataType.TOD, position: 4, returned: {hours: undefined, minutes: undefined, seconds: undefined, hundredths: undefined}}, ], ["date", {type: Zcl.DataType.DATE, position: 4, returned: {year: undefined, month: undefined, dayOfMonth: undefined, dayOfWeek: undefined}}], ["mi struct", {type: Zcl.BuffaloZclDataType.MI_STRUCT, position: 1, returned: {}}], ])("Reads Non-Value for %s", (_name, payload) => { const buffalo = new BuffaloZcl(Buffer.alloc(50, 0xff)); expect(buffalo.read(payload.type, {})).toStrictEqual(payload.returned); expect(buffalo.getPosition()).toStrictEqual(payload.position); }); it.each([ // TODO: others not yet supported [ "time of day", { type: Zcl.DataType.TOD, position: 4, value: {hours: undefined, minutes: undefined, seconds: undefined, hundredths: undefined}, written: [0xff, 0xff, 0xff, 0xff], }, ], [ "date", { type: Zcl.DataType.DATE, position: 4, value: {year: undefined, month: undefined, dayOfMonth: undefined, dayOfWeek: undefined}, written: [0xff, 0xff, 0xff, 0xff], }, ], ])("Writes Non-Value for %s", (_name, payload) => { const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); buffalo.write(payload.type, payload.value, {}); expect(buffalo.getPosition()).toStrictEqual(payload.written.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(payload.written)); }); it.each([ ["time of day", {type: Zcl.DataType.TOD, value: {hours: 1, minutes: 2, seconds: undefined, hundredths: 3}, written: [1, 2, 0xff, 3]}], ["date", {type: Zcl.DataType.DATE, value: {year: 1901, month: 2, dayOfMonth: undefined, dayOfWeek: 3}, written: [1, 2, 0xff, 3]}], ])("Writes & Reads partial Non-Value for %s", (_name, payload) => { const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); buffalo.write(payload.type, payload.value, {}); expect(buffalo.getPosition()).toStrictEqual(payload.written.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(payload.written)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(payload.type, {})).toStrictEqual(payload.value); expect(buffalo.getPosition()).toStrictEqual(payload.written.length); }); it.each([ ["uint32", {type: Zcl.DataType.UINT32, value: 32902534}, {position: 4, write: "writeUInt32", read: "readUInt32"}], ["single prec", {type: Zcl.DataType.SINGLE_PREC, value: 1.539989614439558e-36}, {position: 4, write: "writeFloatLE", read: "readFloatLE"}], ["IEEE address", {type: Zcl.DataType.IEEE_ADDR, value: "0xfe1234abcd9876ff"}, {position: 8, write: "writeIeeeAddr", read: "readIeeeAddr"}], ])("Writes & Reads Use Data Type for %s", (_name, payload, expected) => { const buffer = Buffer.alloc(255); const buffalo = new BuffaloZcl(buffer); // @ts-expect-error dynamic const writeSpy = vi.spyOn(buffalo, expected.write); // @ts-expect-error dynamic const readSpy = vi.spyOn(buffalo, expected.read); buffalo.write(Zcl.BuffaloZclDataType.USE_DATA_TYPE, payload.value, {dataType: payload.type}); expect(writeSpy).toHaveBeenCalledTimes(1); expect(buffalo.getPosition()).toStrictEqual(expected.position); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.BuffaloZclDataType.USE_DATA_TYPE, {dataType: payload.type})).toStrictEqual(payload.value); expect(readSpy).toHaveBeenCalledTimes(1); expect(buffalo.getPosition()).toStrictEqual(expected.position); }); it("Writes & Reads Use Data Type as buffer when missing dataType option", () => { const value = [12, 34]; const buffer = Buffer.alloc(2); const buffalo = new BuffaloZcl(buffer); const writeSpy = vi.spyOn(buffalo, "writeBuffer"); const readSpy = vi.spyOn(buffalo, "readBuffer"); buffalo.write(Zcl.BuffaloZclDataType.USE_DATA_TYPE, value, {}); expect(writeSpy).toHaveBeenCalledTimes(1); expect(writeSpy).toHaveBeenCalledWith(value, value.length); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.BuffaloZclDataType.USE_DATA_TYPE, {})).toStrictEqual(Buffer.from(value)); expect(readSpy).toHaveBeenCalledTimes(1); expect(readSpy).toHaveBeenCalledWith(value.length); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.BuffaloZclDataType.USE_DATA_TYPE, {length: 1})).toStrictEqual(Buffer.from([value[0]])); expect(readSpy).toHaveBeenCalledTimes(2); expect(readSpy).toHaveBeenCalledWith(1); }); it("Throws when write Use Data Type is missing dataType option and value isnt buffer or number array", () => { expect(() => { const payload = 1; const buffalo = new BuffaloZcl(Buffer.alloc(2)); buffalo.write(Zcl.BuffaloZclDataType.USE_DATA_TYPE, payload, {}); }).toThrow(); }); it.each([ Zcl.BuffaloZclDataType.LIST_UINT8, Zcl.BuffaloZclDataType.LIST_UINT16, Zcl.BuffaloZclDataType.LIST_UINT24, Zcl.BuffaloZclDataType.LIST_UINT32, Zcl.BuffaloZclDataType.LIST_ZONEINFO, ])("Throws when read %s is missing required length option", (type) => { expect(() => { const buffalo = new BuffaloZcl(Buffer.alloc(1)); buffalo.read(type, {}); }).toThrow(); }); it("Writes & Reads zone info list", () => { const value = [ {zoneID: 1, zoneStatus: 5}, {zoneID: 2, zoneStatus: 6}, ]; const expectedWritten = [1, 5, 0 /*uint16*/, 2, 6, 0 /*uint16*/]; const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.BuffaloZclDataType.LIST_ZONEINFO, value, {}); expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedWritten)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.BuffaloZclDataType.LIST_ZONEINFO, {length: value.length})).toStrictEqual(value); expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); }); it.each([ ["6" /*uint8 x1*/, {value: [{clstId: 6, len: 1, extField: [1]}]}, {written: [...uint16To8Array(6), 1, 1]}], ["8" /*uint8 x1*/, {value: [{clstId: 8, len: 1, extField: [2]}]}, {written: [...uint16To8Array(8), 1, 2]}], ["258" /*uint8 x2*/, {value: [{clstId: 258, len: 2, extField: [1, 2]}]}, {written: [...uint16To8Array(258), 2, 1, 2]}], [ "768" /*uint16 x3, uint8 x3, uint16 x2*/, {value: [{clstId: 768, len: 13, extField: [1, 2, 3, 4, 5, 6, 7, 8]}]}, { written: [ ...uint16To8Array(768), 13, ...uint16To8Array(1), ...uint16To8Array(2), ...uint16To8Array(3), 4, 5, 6, ...uint16To8Array(7), ...uint16To8Array(8), ], }, ], ])("Writes & Reads Extension Field Sets for data type %s", (_name, payload, expected) => { const buffer = Buffer.alloc(expected.written.length); // XXX: can't be arbitrary atm, see impl for identified issue const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.BuffaloZclDataType.EXTENSION_FIELD_SETS, payload.value, {}); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.BuffaloZclDataType.EXTENSION_FIELD_SETS, {})).toStrictEqual(payload.value); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); }); it.each([ [ "single all options", {value: [{transitionTime: 3, heatSetpoint: 20, coolSetpoint: 15}], readOptions: {payload: {mode: 0b11, numoftrans: 1}}}, {written: [...uint16To8Array(3), ...uint16To8Array(20), ...uint16To8Array(15)]}, ], [ "single heat-only", {value: [{transitionTime: 60, heatSetpoint: 25}], readOptions: {payload: {mode: 0b01, numoftrans: 1}}}, {written: [...uint16To8Array(60), ...uint16To8Array(25)]}, ], [ "single cool-only", {value: [{transitionTime: 256, coolSetpoint: 15}], readOptions: {payload: {mode: 0b10, numoftrans: 1}}}, {written: [...uint16To8Array(256), ...uint16To8Array(15)]}, ], [ "multiple all options", { value: [ {transitionTime: 3, heatSetpoint: 20, coolSetpoint: 15}, {transitionTime: 7, heatSetpoint: 8, coolSetpoint: 3}, {transitionTime: 257, heatSetpoint: 256, coolSetpoint: 0}, ], readOptions: {payload: {mode: 0b11, numoftrans: 3}}, }, { written: [ ...uint16To8Array(3), ...uint16To8Array(20), ...uint16To8Array(15), ...uint16To8Array(7), ...uint16To8Array(8), ...uint16To8Array(3), ...uint16To8Array(257), ...uint16To8Array(256), ...uint16To8Array(0), ], }, ], [ "multiple heat-only", { value: [ {transitionTime: 3, heatSetpoint: 20}, {transitionTime: 70, heatSetpoint: 8}, ], readOptions: {payload: {mode: 0b01, numoftrans: 2}}, }, {written: [...uint16To8Array(3), ...uint16To8Array(20), ...uint16To8Array(70), ...uint16To8Array(8)]}, ], [ "multiple cool-only", { value: [ {transitionTime: 3, coolSetpoint: 15}, {transitionTime: 65000, coolSetpoint: 3}, ], readOptions: {payload: {mode: 0b10, numoftrans: 2}}, }, {written: [...uint16To8Array(3), ...uint16To8Array(15), ...uint16To8Array(65000), ...uint16To8Array(3)]}, ], ])("Writes & Reads Thermo Transitions List", (_name, payload, expected) => { const buffer = Buffer.alloc(50); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, payload.value, {}); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, payload.readOptions)).toStrictEqual(payload.value); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); }); it("Throws when read Thermo Transitions List is missing required payload options", () => { expect(() => { const buffalo = new BuffaloZcl(Buffer.alloc(1)); buffalo.read(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, {}); }).toThrow(); expect(() => { const buffalo = new BuffaloZcl(Buffer.alloc(1)); buffalo.read(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, {payload: {}}); }).toThrow(); expect(() => { const buffalo = new BuffaloZcl(Buffer.alloc(1)); buffalo.read(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, {payload: {mode: 1}}); }).toThrow(); expect(() => { const buffalo = new BuffaloZcl(Buffer.alloc(1)); buffalo.read(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, {payload: {numoftrans: 1}}); }).toThrow(); }); describe("GPD Frame", () => { it("Reads unhandled command as object[raw] if buffer still has bytes to read", () => { const value = [0xff, 0x00]; const buffalo = new BuffaloZcl(Buffer.from(value)); expect(buffalo.read(Zcl.BuffaloZclDataType.GPD_FRAME, {payload: {commandID: 0x1ff, payloadSize: 2}})).toStrictEqual({ raw: Buffer.from(value), }); }); it("Reads unhandled command as object[raw] and ignores GPP data/mic", () => { let value = [0xff, 0x00, 0x21, 0x43, 0xfe]; let buffalo = new BuffaloZcl(Buffer.from(value)); expect(buffalo.read(Zcl.BuffaloZclDataType.GPD_FRAME, {payload: {commandID: 0x1ff, payloadSize: 2}})).toStrictEqual({ raw: Buffer.from(value).subarray(0, -3), }); value = [0xff, 0x00, 0x21, 0x43, 0xfe, 0xf1, 0xf2, 0xf3, 0xf4]; buffalo = new BuffaloZcl(Buffer.from(value)); expect(buffalo.read(Zcl.BuffaloZclDataType.GPD_FRAME, {payload: {commandID: 0x1ff, payloadSize: 2}})).toStrictEqual({ raw: Buffer.from(value).subarray(0, -7), }); }); it("Reads unhandled command as empty object if buffer finished reading", () => { // XXX: this is no longer relevant with proper payload size checking? const value = [0xff, 0x00]; const buffalo = new BuffaloZcl(Buffer.from(value), value.length /* pos at end*/); expect(buffalo.read(Zcl.BuffaloZclDataType.GPD_FRAME, {payload: {commandID: 0x1ff, payloadSize: 2}})).toStrictEqual({}); }); it("Writes commissioning", () => { const expected = [1 /*length*/, 0 /*options*/]; const buffalo = new BuffaloZcl(Buffer.alloc(2)); buffalo.write( Zcl.BuffaloZclDataType.GPD_FRAME, { commandID: 0xf0, options: 0, panID: 0, securityKey: Buffer.alloc(16), keyMic: 0, frameCounter: 0, }, {}, ); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected)); }); it("Reads commissioning", () => { const value = [0xff /*device*/, 0x00 /*options*/]; const buffalo = new BuffaloZcl(Buffer.from(value)); expect(buffalo.read(Zcl.BuffaloZclDataType.GPD_FRAME, {payload: {commandID: 0xe0}})).toStrictEqual({ deviceID: 0xff, options: 0x00, extendedOptions: 0x00, securityKey: Buffer.alloc(16), keyMic: 0, outgoingCounter: 0, manufacturerID: 0, modelID: 0, numGpdCommands: 0, gpdCommandIdList: Buffer.alloc(0), numServerClusters: 0, numClientClusters: 0, gpdServerClusters: Buffer.alloc(0), gpdClientClusters: Buffer.alloc(0), applicationInfo: 0x00, genericSwitchConfig: 0, currentContactStatus: 0, }); }); it("Writes commissioning all options", () => { const expected = [ 27, // length 0b11111, // options 0xff, 0xff, // PAN ID 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // security key 0, 0, 0, 0, // key mic 0, 0, 0, 0, // frame counter ]; const buffalo = new BuffaloZcl(Buffer.alloc(28)); buffalo.write( Zcl.BuffaloZclDataType.GPD_FRAME, { commandID: 0xf0, options: 0b11111, panID: 0xffff, securityKey: Buffer.alloc(16), keyMic: 0, frameCounter: 0, }, {}, ); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected)); }); it("Reads commissioning all options", () => { const value = [ 0xff, // device 0x80 | 0x04, // options 0x20 | 0x40 | 0x80, // extended options 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // security key 0, 0, 0, 0, // key mic 0, 0, 0, 0, // outgoing counter 0x01 | 0x02 | 0x04 | 0x08 | 0x10, // application info 0, 0, // manufacturer ID 0, 0, // model ID 0, // num GPD commands + commands 0, // clusters 2, // switch info length 5, // generic switch config 2, // current contact status ]; const buffalo = new BuffaloZcl(Buffer.from(value)); expect(buffalo.read(Zcl.BuffaloZclDataType.GPD_FRAME, {payload: {commandID: 0xe0}})).toStrictEqual({ deviceID: 0xff, options: 0x80 | 0x04, extendedOptions: 0x20 | 0x40 | 0x80, securityKey: Buffer.alloc(16), keyMic: 0, outgoingCounter: 0, manufacturerID: 0, modelID: 0, numGpdCommands: 0, gpdCommandIdList: Buffer.alloc(0), numServerClusters: 0, numClientClusters: 0, gpdServerClusters: Buffer.alloc(0), gpdClientClusters: Buffer.alloc(0), applicationInfo: 0x01 | 0x02 | 0x04 | 0x08 | 0x10, genericSwitchConfig: 5, currentContactStatus: 2, }); }); it("Writes channel configuration", () => { const expected = [1 /*length*/, 0xf /*Channel 26*/]; const buffalo = new BuffaloZcl(Buffer.alloc(2)); buffalo.write( Zcl.BuffaloZclDataType.GPD_FRAME, { commandID: 0xf3, operationalChannel: 0xf, basic: false, }, {}, ); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected)); }); it("Writes channel configuration basic", () => { const expected = [1 /*length*/, 0x1f /*Channel 26 + Basic*/]; const buffalo = new BuffaloZcl(Buffer.alloc(2)); buffalo.write( Zcl.BuffaloZclDataType.GPD_FRAME, { commandID: 0xf3, operationalChannel: 0xf, basic: true, }, {}, ); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected)); }); it("Reads channel request", () => { const value = [0xfa]; const buffalo = new BuffaloZcl(Buffer.from(value)); expect(buffalo.read(Zcl.BuffaloZclDataType.GPD_FRAME, {payload: {commandID: 0xe3}})).toStrictEqual({ nextChannel: 0xa, nextNextChannel: 0xf, }); }); it("Reads attribute report", () => { const value = [ 0x12, 0x34, // Manufacturer ID 0xff, 0xff, // Cluster ID 0x00, 0x00, // Attribute ID Zcl.DataType.UINT32, // Attribute Type 0x00, 0x01, 0x02, 0x03, 0x01, 0x00, Zcl.DataType.CHAR_STR, 0x06, 0x5a, 0x49, 0x47, 0x42, 0x45, 0x45, 0x02, 0x00, Zcl.DataType.BOOLEAN, 0x01, ]; const buffalo = new BuffaloZcl(Buffer.from(value)); expect(buffalo.read(Zcl.BuffaloZclDataType.GPD_FRAME, {payload: {commandID: 0xa1, payloadSize: value.length}})).toStrictEqual({ manufacturerCode: 13330, clusterID: 65535, attributes: {"0": 50462976, "1": "ZIGBEE", "2": 1}, }); }); it("Writes custom reply", () => { const expected = [ 6, // length 90, 73, 71, 66, 69, 69, // ZIGBEE ]; const buffalo = new BuffaloZcl(Buffer.alloc(7)); buffalo.write(Zcl.BuffaloZclDataType.GPD_FRAME, {commandID: 0xf4, buffer: Buffer.from("ZIGBEE")}, {}); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected)); }); it("Writes nothing for unhandled command", () => { const buffalo = new BuffaloZcl(Buffer.alloc(7)); buffalo.write(Zcl.BuffaloZclDataType.GPD_FRAME, {commandID: 0x1ff}, {}); expect(buffalo.getWritten()).toStrictEqual(Buffer.alloc(0)); }); it("Throws when read is missing payload.payloadSize option when payload.commandID is 0xA1", () => { expect(() => { const buffalo = new BuffaloZcl(Buffer.alloc(1)); buffalo.read(Zcl.BuffaloZclDataType.GPD_FRAME, {payload: {commandID: 0xa1}}); }).toThrow("Cannot read GPD_FRAME with commandID=0xA1 without payloadSize options specified"); }); }); it.each([ ["whole", {value: {indicatorType: Zcl.StructuredIndicatorType.Whole}}, {written: [Zcl.StructuredIndicatorType.Whole]}], [ "indexes only", {value: {indexes: [3, 4, 5, 256]}}, {written: [4, ...uint16To8Array(3), ...uint16To8Array(4), ...uint16To8Array(5), ...uint16To8Array(256)]}, ], ])("Writes & Reads Structured Selector for %s", (_name, payload, expected) => { const buffer = Buffer.alloc(50); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, payload.value, {}); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, {})).toStrictEqual(payload.value); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); }); it.each([ ["Add", {value: {indicatorType: Zcl.StructuredIndicatorType.WriteAdd}}, {written: [Zcl.StructuredIndicatorType.WriteAdd]}], ["Remove", {value: {indicatorType: Zcl.StructuredIndicatorType.WriteRemove}}, {written: [Zcl.StructuredIndicatorType.WriteRemove]}], ])("Writes Structured Selector for %s", (_name, payload, expected) => { const buffer = Buffer.alloc(50); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, payload.value, {}); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); }); it("Throws when read Strctured Select indicator is outside range", () => { expect(() => { const buffer = Buffer.from([17]); const buffalo = new BuffaloZcl(buffer); buffalo.read(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, {}); }).toThrow(); expect(() => { const buffer = Buffer.from([17, 1, 2, 3]); const buffalo = new BuffaloZcl(buffer); buffalo.read(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, {}); }).toThrow(); }); it.each([ [ "single", {value: [{dp: 254, datatype: 125, data: Buffer.from([1, 3, 5])}]}, {written: [254, 125, ...uint16To8Array(3).reverse() /*BE*/, 1, 3, 5]}, ], [ "multiple", { value: [ {dp: 254, datatype: 125, data: Buffer.from([1, 3, 5])}, {dp: 125, datatype: 254, data: Buffer.from([5, 0, 1, 5])}, ], }, {written: [254, 125, ...uint16To8Array(3).reverse() /*BE*/, 1, 3, 5, 125, 254, ...uint16To8Array(4).reverse() /*BE*/, 5, 0, 1, 5]}, ], ])("Writes & Reads Tuya Data Point Values List %s", (_name, payload, expected) => { const buffer = Buffer.alloc(expected.written.length); // XXX: can't be arbitrary atm, see impl for identified issue const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.BuffaloZclDataType.LIST_TUYA_DATAPOINT_VALUES, payload.value, {}); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.BuffaloZclDataType.LIST_TUYA_DATAPOINT_VALUES, {})).toStrictEqual(payload.value); expect(buffalo.getPosition()).toStrictEqual(expected.written.length); }); it("Reads invalid Tuya Data Point Values List as empty array", () => { // incomplete const buffer = Buffer.from([254, 125]); const buffalo = new BuffaloZcl(buffer); expect(buffalo.read(Zcl.BuffaloZclDataType.LIST_TUYA_DATAPOINT_VALUES, {})).toStrictEqual([]); }); it("Writes & Reads Mi Boxer Zones List", () => { const value = [ {zoneNum: 1, groupId: 0x2b84}, {zoneNum: 2, groupId: 0x2b98}, {zoneNum: 3, groupId: 0x2bac}, {zoneNum: 4, groupId: 0x2bc0}, {zoneNum: 5, groupId: 0x2bd4}, {zoneNum: 6, groupId: 0x2be8}, {zoneNum: 7, groupId: 0x2bfc}, {zoneNum: 8, groupId: 0x2c10}, ]; const expectedWritten = [ value.length, ...uint16To8Array(value[0].groupId), value[0].zoneNum, ...uint16To8Array(value[1].groupId), value[1].zoneNum, ...uint16To8Array(value[2].groupId), value[2].zoneNum, ...uint16To8Array(value[3].groupId), value[3].zoneNum, ...uint16To8Array(value[4].groupId), value[4].zoneNum, ...uint16To8Array(value[5].groupId), value[5].zoneNum, ...uint16To8Array(value[6].groupId), value[6].zoneNum, ...uint16To8Array(value[7].groupId), value[7].zoneNum, ]; const buffer = Buffer.alloc(50); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.BuffaloZclDataType.LIST_MIBOXER_ZONES, value, {}); expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedWritten)); // @ts-expect-error private buffalo.position = 0; expect(buffalo.read(Zcl.BuffaloZclDataType.LIST_MIBOXER_ZONES, {length: value.length})).toStrictEqual(value); expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); }); it("Writes & Reads big endian uint24", () => { const value = 16777200; const expectedWritten = [0xff, 0xff, 0xf0]; const expectedPosition = 3; const buffer = Buffer.alloc(10); const buffalo = new BuffaloZcl(buffer); buffalo.write(Zcl.BuffaloZclDataType.BIG_ENDIAN_UINT24, value, {}); expect(buffalo.getPosition()).toStrictEqual(expectedPosition); expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedWritten)); // @ts-e