zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
1,225 lines (1,103 loc) • 50.2 kB
text/typescript
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