zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
1,197 lines (1,043 loc) • 59.7 kB
text/typescript
import type {ZclPayload} from "../src/adapter/events";
import type {MockInstance} from "vitest";
import {GreenPower} from "../src/controller/greenPower";
import type {GreenPowerDeviceJoinedPayload} from "../src/controller/tstype";
import {logger} from "../src/utils/logger";
import {GP_ENDPOINT, GP_GROUP_ID} from "../src/zspec/consts";
import * as Zcl from "../src/zspec/zcl";
describe("GreenPower", () => {
let gp: GreenPower;
let logDebugSpy: MockInstance;
let logInfoSpy: MockInstance;
let logWarningSpy: MockInstance;
let logErrorSpy: MockInstance;
const clearLogMocks = (): void => {
logDebugSpy.mockClear();
logInfoSpy.mockClear();
logWarningSpy.mockClear();
logErrorSpy.mockClear();
};
const makeNotificationOptions = (
applicationId: number,
gpdfSecurityLevel: number,
gpdfSecurityKeyType: number,
bidirectionalInfo: number,
): number => {
return (applicationId & 0x7) | ((gpdfSecurityLevel & 0x3) << 6) | ((gpdfSecurityKeyType & 0x7) << 8) | ((bidirectionalInfo & 0x3) << 11);
};
const makeHeader = (
sequenceNumber: number,
commandIdentifier: number,
applicationId: number,
gpdfSecurityLevel: number,
gpdfSecurityKeyType: number,
bidirectionalInfo: number,
sourceId: number,
gpdSecurityFrameCounter: number,
gpdCommandId: number,
payloadLength: number,
options?: number,
): Buffer => {
const gpdHeader = Buffer.alloc(15);
gpdHeader.writeUInt8(0b00000001, 0); // frameControl: FrameType.SPECIFIC + Direction.CLIENT_TO_SERVER + disableDefaultResponse=false
gpdHeader.writeUInt8(sequenceNumber, 1);
gpdHeader.writeUInt8(commandIdentifier, 2);
gpdHeader.writeUInt16LE(options ?? makeNotificationOptions(applicationId, gpdfSecurityLevel, gpdfSecurityKeyType, bidirectionalInfo), 3);
gpdHeader.writeUInt32LE(sourceId, 5);
gpdHeader.writeUInt32LE(gpdSecurityFrameCounter, 9);
gpdHeader.writeUInt8(gpdCommandId, 13);
gpdHeader.writeUInt8(payloadLength, 14);
return gpdHeader;
};
const makeFooter = (options: number, gppNwkAddr?: number, gppGpdLink?: number, mic?: number): Buffer => {
const hasGppData = options & 0x800;
const hasMic = options & 0x200;
const gpdFooter = Buffer.alloc((hasGppData ? 3 : 0) + (hasMic ? 4 : 0));
if (hasGppData) {
gpdFooter.writeUInt16LE(gppNwkAddr!, 0);
gpdFooter.writeUInt8(gppGpdLink!, 2);
}
if (hasMic) {
gpdFooter.writeUInt32LE(mic!, hasGppData ? 3 : 0);
}
return gpdFooter;
};
const makePayload = (sourceId: number, buffer: Buffer, linkQuality: number): ZclPayload => {
return {
clusterID: Zcl.Clusters.greenPower.ID,
header: Zcl.Header.fromBuffer(buffer),
address: sourceId & 0xffff,
data: buffer,
endpoint: GP_ENDPOINT,
linkquality: linkQuality,
groupID: GP_GROUP_ID,
wasBroadcast: true,
destinationEndpoint: GP_ENDPOINT,
};
};
beforeAll(() => {
vi.useFakeTimers();
logDebugSpy = vi.spyOn(logger, "debug");
logInfoSpy = vi.spyOn(logger, "info");
logWarningSpy = vi.spyOn(logger, "warning");
logErrorSpy = vi.spyOn(logger, "error");
});
beforeEach(() => {
clearLogMocks();
gp = new GreenPower(
// @ts-expect-error minimal mock
{
getCoordinatorIEEE: vi.fn(),
sendZclFrameToAll: vi.fn(),
sendZclFrameToEndpoint: vi.fn(),
getNetworkParameters: vi.fn(),
},
);
});
afterAll(() => {
vi.useRealTimers();
});
it("encodes & decodes pairing options", () => {
let rawByte = 0b000000000110101000;
let rawOptions = {
appId: 0,
addSink: true,
removeGpd: false,
communicationMode: 0b01,
gpdFixed: true,
gpdMacSeqNumCapabilities: true,
securityLevel: 0,
securityKeyType: 0,
gpdSecurityFrameCounterPresent: false,
gpdSecurityKeyPresent: false,
assignedAliasPresent: false,
groupcastRadiusPresent: false,
};
let options = GreenPower.decodePairingOptions(rawByte);
let byte = GreenPower.encodePairingOptions(rawOptions);
expect(options).toStrictEqual(rawOptions);
expect(rawByte).toStrictEqual(byte);
rawByte = 0b001110010101001000;
rawOptions = {
appId: 0,
addSink: true,
removeGpd: false,
communicationMode: 0b10,
gpdFixed: false,
gpdMacSeqNumCapabilities: true,
securityLevel: 0b10,
securityKeyType: 0b100,
gpdSecurityFrameCounterPresent: true,
gpdSecurityKeyPresent: true,
assignedAliasPresent: false,
groupcastRadiusPresent: false,
};
options = GreenPower.decodePairingOptions(rawByte);
byte = GreenPower.encodePairingOptions(rawOptions);
expect(options).toStrictEqual(rawOptions);
expect(rawByte).toStrictEqual(byte);
rawByte = 0b001110010101101000;
rawOptions = {
appId: 0,
addSink: true,
removeGpd: false,
communicationMode: 0b11,
gpdFixed: false,
gpdMacSeqNumCapabilities: true,
securityLevel: 0b10,
securityKeyType: 0b100,
gpdSecurityFrameCounterPresent: true,
gpdSecurityKeyPresent: true,
assignedAliasPresent: false,
groupcastRadiusPresent: false,
};
options = GreenPower.decodePairingOptions(rawByte);
byte = GreenPower.encodePairingOptions(rawOptions);
expect(options).toStrictEqual(rawOptions);
expect(rawByte).toStrictEqual(byte);
rawByte = 0b000000000110110000;
rawOptions = {
appId: 0,
addSink: false,
removeGpd: true,
communicationMode: 0b01,
gpdFixed: true,
gpdMacSeqNumCapabilities: true,
securityLevel: 0b00,
securityKeyType: 0b000,
gpdSecurityFrameCounterPresent: false,
gpdSecurityKeyPresent: false,
assignedAliasPresent: false,
groupcastRadiusPresent: false,
};
options = GreenPower.decodePairingOptions(rawByte);
byte = GreenPower.encodePairingOptions(rawOptions);
expect(options).toStrictEqual(rawOptions);
expect(rawByte).toStrictEqual(byte);
// coverage
rawByte = 0b110000000010110000;
rawOptions = {
appId: 0,
addSink: false,
removeGpd: true,
communicationMode: 0b01,
gpdFixed: true,
gpdMacSeqNumCapabilities: false,
securityLevel: 0b00,
securityKeyType: 0b000,
gpdSecurityFrameCounterPresent: false,
gpdSecurityKeyPresent: false,
assignedAliasPresent: true,
groupcastRadiusPresent: true,
};
options = GreenPower.decodePairingOptions(rawByte);
byte = GreenPower.encodePairingOptions(rawOptions);
expect(options).toStrictEqual(rawOptions);
expect(rawByte).toStrictEqual(byte);
});
it("encodes & decodes commissioning mode options", () => {
let rawByte = 0x0b;
let rawOptions = {action: 1, commissioningWindowPresent: true, exitMode: 0b10, channelPresent: false, unicastCommunication: false};
let options = GreenPower.decodeCommissioningModeOptions(rawByte);
let byte = GreenPower.encodeCommissioningModeOptions(rawOptions);
expect(options).toStrictEqual(rawOptions);
expect(rawByte).toStrictEqual(byte);
rawByte = 0x2b;
rawOptions = {action: 1, commissioningWindowPresent: true, exitMode: 0b10, channelPresent: false, unicastCommunication: true};
options = GreenPower.decodeCommissioningModeOptions(rawByte);
byte = GreenPower.encodeCommissioningModeOptions(rawOptions);
expect(options).toStrictEqual(rawOptions);
expect(rawByte).toStrictEqual(byte);
rawByte = 0x0a;
rawOptions = {action: 0, commissioningWindowPresent: true, exitMode: 0b10, channelPresent: false, unicastCommunication: false};
options = GreenPower.decodeCommissioningModeOptions(rawByte);
byte = GreenPower.encodeCommissioningModeOptions(rawOptions);
expect(options).toStrictEqual(rawOptions);
expect(rawByte).toStrictEqual(byte);
expect(options).toStrictEqual(rawOptions);
expect(rawByte).toStrictEqual(byte);
// coverage
rawByte = 0b111100;
rawOptions = {action: 0, commissioningWindowPresent: false, exitMode: 0b11, channelPresent: true, unicastCommunication: true};
options = GreenPower.decodeCommissioningModeOptions(rawByte);
byte = GreenPower.encodeCommissioningModeOptions(rawOptions);
expect(options).toStrictEqual(rawOptions);
expect(rawByte).toStrictEqual(byte);
});
it("omits GPP data from raw payload", async () => {
const addr = {applicationId: 0, sourceId: 2777252112, endpoint: 0};
const options = 0x800;
const sequenceNumber = 18;
const gpdSecurityFrameCounter = 17326;
const gpdCommandId = 38;
const gpdCommandPayload = Buffer.from([0x3e]);
const commandIdentifier = Zcl.Clusters.greenPower.commands.commissioningNotification.ID;
const gppNwkAddr = 24404;
const gppGpdLink = 207;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
0,
0,
0,
0,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
options,
);
const gpdFooter = makeFooter(options, gppNwkAddr, gppGpdLink);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload, gpdFooter]), 138);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, undefined);
expect(frame.payload.gppNwkAddr).toStrictEqual(gppNwkAddr);
expect(frame.payload.gppGpdLink).toStrictEqual(gppGpdLink);
expect(retFrame.payload.commandFrame).toStrictEqual({raw: gpdCommandPayload});
expect(retFrame.payload.gppNwkAddr).toStrictEqual(gppNwkAddr);
expect(retFrame.payload.gppGpdLink).toStrictEqual(gppGpdLink);
});
it("omits MIC from raw payload", async () => {
const securityKey = Buffer.from([227, 227, 225, 134, 235, 104, 141, 250, 162, 211, 104, 147, 201, 146, 67, 175]);
const addr = {applicationId: 0, sourceId: 2777252112, endpoint: 0};
const options = 0x30 | 0x200;
const sequenceNumber = 18;
const gpdSecurityFrameCounter = 17326;
const gpdCommandId = 38;
const gpdCommandPayload = Buffer.from([0x3e]);
const commandIdentifier = Zcl.Clusters.greenPower.commands.commissioningNotification.ID;
const mic = 1441399364;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
0,
0,
0,
0,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
options,
);
const gpdFooter = makeFooter(options, undefined, undefined, mic);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload, gpdFooter]), 138);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, securityKey);
expect(frame.payload.mic).toBeDefined(); // garbage
expect(retFrame.payload.commandID).toStrictEqual(0x21); // just to be sure it decrypted properly
expect(retFrame.payload.commandFrame).toStrictEqual({raw: Buffer.from([207 /* decrypted, bogus data */])});
expect(retFrame.payload.mic).toStrictEqual(undefined); // removed once decrypted
});
it("omits GPP data and MIC from raw payload", async () => {
const securityKey = Buffer.from([227, 227, 225, 134, 235, 104, 141, 250, 162, 211, 104, 147, 201, 146, 67, 175]);
const addr = {applicationId: 0, sourceId: 2777252112, endpoint: 0};
const options = 0x30 | 0x200 | 0x800;
const sequenceNumber = 18;
const gpdSecurityFrameCounter = 17326;
const gpdCommandId = 38;
const gpdCommandPayload = Buffer.from([0x3e]);
const commandIdentifier = Zcl.Clusters.greenPower.commands.commissioningNotification.ID;
const gppNwkAddr = 24404;
const gppGpdLink = 207;
const mic = 1441399364;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
0,
0,
0,
0,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
options,
);
const gpdFooter = makeFooter(options, gppNwkAddr, gppGpdLink, mic);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload, gpdFooter]), 138);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, securityKey);
expect(frame.payload.gppNwkAddr).toBeDefined(); // garbage
expect(frame.payload.gppGpdLink).toBeDefined(); // garbage
expect(frame.payload.mic).toBeDefined(); // garbage
expect(retFrame.payload.commandID).toStrictEqual(0x21); // just to be sure it decrypted properly
expect(retFrame.payload.commandFrame).toStrictEqual({raw: Buffer.from([207 /* decrypted, bogus data */])});
expect(retFrame.payload.gppNwkAddr).toStrictEqual(gppNwkAddr); // removed once decrypted
expect(retFrame.payload.gppGpdLink).toStrictEqual(gppGpdLink); // removed once decrypted
expect(retFrame.payload.mic).toStrictEqual(undefined); // removed once decrypted
});
it("does not parse command frame when FULLENCR security level - SINK", async () => {
const addr = {applicationId: 0, sourceId: 2888399791, endpoint: 0};
const securityLevelFullEncr = 3;
const securityKeyTypeNWK = 1;
const gpdLink = 207;
const sequenceNumber = 143;
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 3727;
const gpdCommandId = 227; // this would otherwise be CHANNEL_REQUEST and result in bad parsing
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
securityLevelFullEncr,
securityKeyTypeNWK,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
{
// mock bad frame, allows to bypass options check and validate that this errors out (trying to parse as CHANNEL_REQUEST)
const alteredSecurityGpdHeader = Buffer.from(gpdHeader);
alteredSecurityGpdHeader[3] = 0;
alteredSecurityGpdHeader[4] = 0;
const payload = makePayload(addr.sourceId, Buffer.concat([alteredSecurityGpdHeader, gpdCommandPayload]), gpdLink);
expect(() => {
Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
}).toThrow('The value of "offset" is out of range. It must be >= 0 and <= 14. Received 15');
}
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
expect(frame.payload.commandFrame).toBeUndefined(); // as opposed to `{}` when parsing (payloadSize=0)
const retFrame = await gp.processCommand(payload, frame, Buffer.alloc(16) /* just for the codepath, decrypting not important */);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x9d srcID=2888399791 gpp=NO",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x9d;
clonedFrame.payload.options = 256;
clonedFrame.payload.commandFrame = {};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
});
it("does not parse command frame when FULLENCR security level - GPP", async () => {
const addr = {applicationId: 0, sourceId: 2888399791, endpoint: 0};
const gpdLink = 207;
const sequenceNumber = 143;
const gpdSecurityFrameCounter = 3727;
const gpdCommandId = 227; // this would otherwise be CHANNEL_REQUEST and result in bad parsing
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.commissioningNotification.ID;
const gppNwkAddr = 24404;
const gppGpdLink = 123;
const mic = 456;
const options = 2864;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
0,
0,
0,
0,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
options,
);
const gpdFooter = makeFooter(options, gppNwkAddr, gppGpdLink, mic);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload, gpdFooter]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
expect(frame.payload.commandFrame).toBeUndefined(); // as opposed to `{}` when parsing (payloadSize=0)
const retFrame = await gp.processCommand(payload, frame, Buffer.alloc(16) /* just for the codepath, decrypting not important */);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x9d srcID=2888399791 gpp=24404 rssi=59 linkQuality=Moderate",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x9d;
clonedFrame.payload.options = 2304;
clonedFrame.payload.commandFrame = {};
clonedFrame.payload.gppNwkAddr = gppNwkAddr;
clonedFrame.payload.gppGpdLink = gppGpdLink;
delete clonedFrame.payload.mic;
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
});
// @see https://github.com/Koenkk/zigbee2mqtt/issues/19405#issuecomment-2727338024
it("FULLENCR ZT-LP-ZEU2S-WH-MS MOES 2-gang vectors from ember", async () => {
let joinData: GreenPowerDeviceJoinedPayload | undefined;
gp.on("deviceJoined", (payload) => {
joinData = payload;
});
const addr = {applicationId: 0, sourceId: 1496140231, endpoint: 0};
{
const gpdLink = 214;
const sequenceNumber = 19;
const gpdfSecurityLevel = 0; // NONE
const gpdfSecurityKeyType = 0; // NONE
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 4294967295;
const gpdCommandId = 224;
const gpdCommandPayload = Buffer.from("0289f31adb70a88d71196ee50c03580537767de27ad5331309000037647a62697061304047503030303157", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.commissioningNotification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
gpdfSecurityLevel,
gpdfSecurityKeyType,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey); // always undefined since not yet joined
await vi.waitUntil(() => joinData !== undefined);
expect(joinData).toStrictEqual({
sourceID: addr.sourceId,
deviceID: frame.payload.commandFrame.deviceID,
networkAddress: addr.sourceId & 0xffff,
securityKey: frame.payload.commandFrame.securityKey,
});
expect(logInfoSpy).toHaveBeenNthCalledWith(1, "[COMMISSIONING] srcID=1496140231 gpp=NO", "zh:controller:greenpower");
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[PAIRING] srcID=1496140231 gpp=NO options=58696 (addSink=true commMode=2)",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0xe0;
clonedFrame.payload.options = 0;
clonedFrame.payload.commandFrame = {
deviceID: 2,
options: 137,
extendedOptions: 243,
securityKey: joinData?.securityKey,
keyMic: 869628642,
outgoingCounter: 2323,
applicationInfo: addr.applicationId,
manufacturerID: 0,
modelID: 0,
numGpdCommands: 0,
gpdCommandIdList: Buffer.from([]),
numServerClusters: 0,
numClientClusters: 0,
gpdServerClusters: Buffer.from([]),
gpdClientClusters: Buffer.from([]),
genericSwitchConfig: 0,
currentContactStatus: 0,
};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame.securityKey).toStrictEqual(joinData?.securityKey);
}
clearLogMocks();
const securityLevelFullEncr = 3;
const securityKeyTypeNWK = 1;
// left
{
const gpdLink = 220;
const sequenceNumber = 28;
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 2332;
const gpdCommandId = 136;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
securityLevelFullEncr,
securityKeyTypeNWK,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x20 srcID=1496140231 gpp=NO",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x20;
clonedFrame.payload.options = 256;
clonedFrame.payload.commandFrame = {};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame).toStrictEqual({});
}
clearLogMocks();
// left
{
const gpdLink = 220;
const sequenceNumber = 46;
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 2350;
const gpdCommandId = 152;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
securityLevelFullEncr,
securityKeyTypeNWK,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x20 srcID=1496140231 gpp=NO",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x20;
clonedFrame.payload.options = 256;
clonedFrame.payload.commandFrame = {};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame).toStrictEqual({});
}
clearLogMocks();
// left
{
const gpdLink = 223;
const sequenceNumber = 55;
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 2359;
const gpdCommandId = 189;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
securityLevelFullEncr,
securityKeyTypeNWK,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x20 srcID=1496140231 gpp=NO",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x20;
clonedFrame.payload.options = 256;
clonedFrame.payload.commandFrame = {};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame).toStrictEqual({});
}
clearLogMocks();
// right
{
const gpdLink = 218;
const sequenceNumber = 37;
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 2341;
const gpdCommandId = 172;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
securityLevelFullEncr,
securityKeyTypeNWK,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x21 srcID=1496140231 gpp=NO",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x21;
clonedFrame.payload.options = 256;
clonedFrame.payload.commandFrame = {};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame).toStrictEqual({});
}
clearLogMocks();
// right
{
const gpdLink = 222;
const sequenceNumber = 64;
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 2368;
const gpdCommandId = 159;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
securityLevelFullEncr,
securityKeyTypeNWK,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x21 srcID=1496140231 gpp=NO",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x21;
clonedFrame.payload.options = 256;
clonedFrame.payload.commandFrame = {};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame).toStrictEqual({});
}
clearLogMocks();
// right
{
const gpdLink = 222;
const sequenceNumber = 73;
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 2377;
const gpdCommandId = 11;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
securityLevelFullEncr,
securityKeyTypeNWK,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x21 srcID=1496140231 gpp=NO",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x21;
clonedFrame.payload.options = 256;
clonedFrame.payload.commandFrame = {};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame).toStrictEqual({});
}
clearLogMocks();
// mock FULLENCR with unknown security key
{
const gpdLink = 222;
const sequenceNumber = 73;
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 2377;
const gpdCommandId = 11;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
securityLevelFullEncr,
securityKeyTypeNWK,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, undefined);
expect(logErrorSpy).toHaveBeenNthCalledWith(
1,
"[FULLENCR] srcID=1496140231 gpp=NO commandIdentifier=0 Unknown security key",
"zh:controller:greenpower",
);
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(frame)));
}
clearLogMocks();
// mock FULLENCR with gpp data
{
const gpdLink = 222;
const sequenceNumber = 73;
const gpdSecurityFrameCounter = 2377;
const gpdCommandId = 11;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gppNwkAddr = 24404;
const gppGpdLink = 207;
const options = ((0b11 & 0x3) << 6) | 0x4000;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
0,
0,
0,
0,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
options,
);
const gpdFooter = Buffer.alloc(3);
gpdFooter.writeUInt16LE(gppNwkAddr, 0);
gpdFooter.writeUInt8(gppGpdLink, 2);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload, gpdFooter]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x21 srcID=1496140231 gpp=24404 rssi=15 linkQuality=Excellent",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x21;
clonedFrame.payload.options = 16384;
clonedFrame.payload.commandFrame = {};
clonedFrame.payload.gppNwkAddr = gppNwkAddr;
clonedFrame.payload.gppGpdLink = gppGpdLink;
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame).toStrictEqual({});
}
});
// @see https://github.com/Koenkk/zigbee2mqtt/issues/19405#issuecomment-2732204071
it("FULLENCR ZT-LP-ZEU2S-WH-MS MOES 3-gang vectors from ember", async () => {
let joinData: GreenPowerDeviceJoinedPayload | undefined;
gp.on("deviceJoined", (payload) => {
joinData = payload;
});
const addr = {applicationId: 0, sourceId: 344902069, endpoint: 0};
{
const gpdLink = 219;
const sequenceNumber = 139;
const gpdfSecurityLevel = 0; // NONE
const gpdfSecurityKeyType = 0; // NONE
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 4294967295;
const gpdCommandId = 224;
const gpdCommandPayload = Buffer.from("0289f35690230a93ea5f1951926f200236c7820891812a8b0400007165726837706f7840475030303031be", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.commissioningNotification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
gpdfSecurityLevel,
gpdfSecurityKeyType,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey); // always undefined since not yet joined
await vi.waitUntil(() => joinData !== undefined);
expect(joinData).toStrictEqual({
sourceID: addr.sourceId,
deviceID: frame.payload.commandFrame.deviceID,
networkAddress: addr.sourceId & 0xffff,
securityKey: frame.payload.commandFrame.securityKey,
});
expect(logInfoSpy).toHaveBeenNthCalledWith(1, "[COMMISSIONING] srcID=344902069 gpp=NO", "zh:controller:greenpower");
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[PAIRING] srcID=344902069 gpp=NO options=58696 (addSink=true commMode=2)",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0xe0;
clonedFrame.payload.options = 0;
clonedFrame.payload.commandFrame = {
deviceID: 2,
options: 137,
extendedOptions: 243,
securityKey: joinData?.securityKey,
keyMic: 713134344,
outgoingCounter: 1163,
applicationInfo: addr.applicationId,
manufacturerID: 0,
modelID: 0,
numGpdCommands: 0,
gpdCommandIdList: Buffer.from([]),
numServerClusters: 0,
numClientClusters: 0,
gpdServerClusters: Buffer.from([]),
gpdClientClusters: Buffer.from([]),
genericSwitchConfig: 0,
currentContactStatus: 0,
};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame.securityKey).toStrictEqual(joinData?.securityKey);
}
clearLogMocks();
const securityLevelFullEncr = 3;
const securityKeyTypeNWK = 1;
// left
{
const gpdLink = 224;
const sequenceNumber = 175;
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 1199;
const gpdCommandId = 92;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
securityLevelFullEncr,
securityKeyTypeNWK,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x20 srcID=344902069 gpp=NO",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x20;
clonedFrame.payload.options = 256;
clonedFrame.payload.commandFrame = {};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame).toStrictEqual({});
}
clearLogMocks();
// middle
{
const gpdLink = 225;
const sequenceNumber = 184;
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 1208;
const gpdCommandId = 109;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
securityLevelFullEncr,
securityKeyTypeNWK,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x21 srcID=344902069 gpp=NO",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x21;
clonedFrame.payload.options = 256;
clonedFrame.payload.commandFrame = {};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame).toStrictEqual({});
}
clearLogMocks();
// right
{
const gpdLink = 225;
const sequenceNumber = 193;
const bidirectionalInfo = 0;
const gpdSecurityFrameCounter = 1217;
const gpdCommandId = 219;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.notification.ID;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
addr.applicationId,
securityLevelFullEncr,
securityKeyTypeNWK,
bidirectionalInfo,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload]), gpdLink);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x11 srcID=344902069 gpp=NO",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x11;
clonedFrame.payload.options = 256;
clonedFrame.payload.commandFrame = {};
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame).toStrictEqual({});
}
});
// @see https://github.com/Koenkk/zigbee2mqtt/issues/19405#issuecomment-2744667458
it("FULLENCR ZT-LP-ZEU2S-WH-MS MOES 2-gang vectors from zstack through GPP", async () => {
const joinData: GreenPowerDeviceJoinedPayload = {
sourceID: 2777252112,
deviceID: 2,
networkAddress: 2777252112 & 0xffff,
securityKey: Buffer.from([227, 227, 225, 134, 235, 104, 141, 250, 162, 211, 104, 147, 201, 146, 67, 175]),
};
const addr = {applicationId: 0, sourceId: 2777252112, endpoint: 0};
const gppNwkAddr = 24404;
const options = 2864;
// right
{
const sequenceNumber = 18;
const gpdSecurityFrameCounter = 17326;
const gpdCommandId = 38;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.commissioningNotification.ID;
const gppGpdLink = 207;
const mic = 1441399364;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
0,
0,
0,
0,
addr.sourceId,
gpdSecurityFrameCounter,
gpdCommandId,
gpdCommandPayload.length,
options,
);
const gpdFooter = makeFooter(options, gppNwkAddr, gppGpdLink, mic);
const payload = makePayload(addr.sourceId, Buffer.concat([gpdHeader, gpdCommandPayload, gpdFooter]), 138);
const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
const retFrame = await gp.processCommand(payload, frame, joinData?.securityKey);
expect(logDebugSpy).toHaveBeenNthCalledWith(
1,
"[UNHANDLED_CMD/PASSTHROUGH] command=0x21 srcID=2777252112 gpp=24404 rssi=15 linkQuality=Excellent",
"zh:controller:greenpower",
);
const clonedFrame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {});
clonedFrame.payload.commandID = 0x21;
clonedFrame.payload.options = 2304;
clonedFrame.payload.commandFrame = {};
clonedFrame.payload.gppNwkAddr = gppNwkAddr;
clonedFrame.payload.gppGpdLink = gppGpdLink;
delete clonedFrame.payload.mic;
expect(JSON.parse(JSON.stringify(retFrame))).toStrictEqual(JSON.parse(JSON.stringify(clonedFrame)));
expect(retFrame.payload.commandFrame).toStrictEqual({});
}
clearLogMocks();
// right
{
const sequenceNumber = 19;
const gpdSecurityFrameCounter = 17335;
const gpdCommandId = 17;
const gpdCommandPayload = Buffer.from("", "hex");
const commandIdentifier = Zcl.Clusters.greenPower.commands.commissioningNotification.ID;
const gppGpdLink = 207;
const mic = 3064327344;
const gpdHeader = makeHeader(
sequenceNumber,
commandIdentifier,
0,