zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
1,262 lines (1,187 loc) • 162 kB
text/typescript
import * as fs from "node:fs";
import * as path from "node:path";
import equals from "fast-deep-equal/es6";
import type {ZclPayload} from "../../../src/adapter/events";
import {ZnpVersion} from "../../../src/adapter/z-stack/adapter/tstype";
import {ZStackAdapter} from "../../../src/adapter/z-stack/adapter/zStackAdapter";
import * as Constants from "../../../src/adapter/z-stack/constants";
import {AddressMode, DevStates, NvItemsIds, NvSystemIds, type ZnpCommandStatus} from "../../../src/adapter/z-stack/constants/common";
import * as Structs from "../../../src/adapter/z-stack/structs";
import {Subsystem, Type} from "../../../src/adapter/z-stack/unpi/constants";
import {Znp, type ZpiObject} from "../../../src/adapter/z-stack/znp";
import Definition from "../../../src/adapter/z-stack/znp/definition";
import type {ZpiObjectPayload} from "../../../src/adapter/z-stack/znp/tstype";
import type {UnifiedBackupStorage} from "../../../src/models";
import {setLogger} from "../../../src/utils/logger";
import * as ZSpec from "../../../src/zspec";
import {BroadcastAddress} from "../../../src/zspec/enums";
import * as Zcl from "../../../src/zspec/zcl";
import * as Zdo from "../../../src/zspec/zdo";
import type {
ActiveEndpointsResponse,
EndDeviceAnnounce,
LQITableResponse,
NetworkAddressResponse,
NodeDescriptorResponse,
RoutingTableResponse,
SimpleDescriptorResponse,
} from "../../../src/zspec/zdo/definition/tstypes";
const DUMMY_NODE_DESC_RSP_CAPABILITIES = {
allocateAddress: 0,
alternatePANCoordinator: 0,
deviceType: 2,
powerSource: 0,
reserved1: 0,
reserved2: 0,
rxOnWhenIdle: 0,
securityCapability: 0,
};
const mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
};
const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
const mockSetTimeout = () => {
return vi.spyOn(globalThis, "setTimeout").mockImplementation(
// @ts-expect-error mock
(cb) => cb(),
);
};
vi.mock("../../../src/utils/wait", () => ({
wait: vi.fn(() => {
return new Promise<void>((resolve) => resolve());
}),
}));
const waitForResult = (payloadOrPromise: Promise<unknown> | ZpiObjectPayload, id?: number) => {
id = id || 1;
if (payloadOrPromise instanceof Promise) {
return {
start: () => {
return {promise: payloadOrPromise, ID: id};
},
ID: id,
};
}
return {
start: () => {
return {promise: new Promise((r) => r(payloadOrPromise)), ID: id};
},
ID: id,
};
};
const networkOptions = {
panID: 123,
extendedPanID: [0x00, 0x12, 0x4b, 0x00, 0x09, 0xd6, 0x9f, 0x77],
channelList: [21],
networkKey: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
networkKeyDistribute: false,
};
const networkOptionsDefaultExtendedPanId = {
panID: 123,
extendedPanID: [0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd],
channelList: [21],
networkKey: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
networkKeyDistribute: false,
};
const networkOptionsMismatched = {
panID: 124,
extendedPanID: [0x00, 0x12, 0x4b, 0x00, 0x09, 0xd6, 0x9f, 0x77],
channelList: [21],
networkKey: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
networkKeyDistribute: false,
};
const networkOptionsInvalidPanId = {
panID: 65535,
extendedPanID: [0x00, 0x12, 0x4b, 0x00, 0x09, 0xd6, 0x9f, 0x77],
channelList: [21],
networkKey: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
networkKeyDistribute: false,
};
const serialPortOptions = {
baudRate: 800,
rtscts: false,
path: "dummy",
};
const backupMatchingConfig = {
metadata: {
format: "zigpy/open-coordinator-backup",
version: 1,
source: "zigbee-herdsman@0.13.65",
internal: {
date: "2021-03-03T19:15:40.524Z",
znpVersion: 2,
},
},
stack_specific: {
zstack: {
tclk_seed: "928a2c479e72a9a53e3b5133fc55021f",
},
},
coordinator_ieee: "00124b0009d80ba7",
pan_id: "007b",
extended_pan_id: "00124b0009d69f77",
nwk_update_id: 0,
security_level: 5,
channel: 21,
channel_mask: [21],
network_key: {
key: "01030507090b0d0f00020406080a0c0d",
sequence_number: 0,
frame_counter: 16754,
},
devices: [
{
nwk_address: "ddf6",
ieee_address: "00124b002226ef87",
},
{
nwk_address: "c2dc",
ieee_address: "04cf8cdf3c79455f",
link_key: {
key: "0e768569dd935d8e7302e74e7629f13f",
rx_counter: 0,
tx_counter: 275,
},
},
{
nwk_address: "740a",
ieee_address: "680ae2fffeae5647",
link_key: {
key: "7c079d02aae015facd7ae9608d4baf56",
rx_counter: 0,
tx_counter: 275,
},
},
{
nwk_address: "19fa",
ieee_address: "00158d00024fa79b",
link_key: {
key: "cea550908aa1529ee90eea3c3bdc26fc",
rx_counter: 0,
tx_counter: 44,
},
},
{
nwk_address: "6182",
ieee_address: "00158d00024f4518",
link_key: {
key: "267e1e31fcd8171f8acf63459effbca5",
rx_counter: 0,
tx_counter: 44,
},
},
{
nwk_address: "4285",
ieee_address: "00158d00024f810d",
is_child: false,
link_key: {
key: "55ba1e31fcd8171f9f0b63459effbca5",
rx_counter: 0,
tx_counter: 44,
},
},
{
// "nwk_address": "4286", commented because `nwk_address` is optional in the backup
ieee_address: "00158d00024f810e",
is_child: true,
link_key: {
key: "55ba1e31fcd8171fee0b63459effeea5",
rx_counter: 24,
tx_counter: 91,
},
},
],
};
const backupMatchingConfig12 = {
metadata: {
format: "zigpy/open-coordinator-backup",
version: 1,
source: "zigbee-herdsman@0.13.65",
internal: {
date: "2021-03-03T19:15:40.524Z",
znpVersion: 0,
},
},
stack_specific: {
zstack: {},
},
coordinator_ieee: "00124b0009d80ba7",
pan_id: "007b",
extended_pan_id: "00124b0009d69f77",
nwk_update_id: 0,
security_level: 5,
channel: 21,
channel_mask: [21],
network_key: {
key: "01030507090b0d0f00020406080a0c0d",
sequence_number: 0,
frame_counter: 0,
},
devices: [
{
nwk_address: "ddf6",
ieee_address: "00124b002226ef87",
},
],
};
const backupNotMatchingConfig = {
metadata: {
format: "zigpy/open-coordinator-backup",
version: 1,
source: "zigbee-herdsman@0.13.65",
internal: {
date: "2021-03-03T19:15:40.524Z",
znpVersion: 2,
},
},
stack_specific: {
zstack: {
tclk_seed: "928a2c479e72a9a53e3b5133fc55021f",
},
},
coordinator_ieee: "00124b0009d80ba7",
pan_id: "007c",
extended_pan_id: "00124b0009d69f77",
nwk_update_id: 0,
security_level: 5,
channel: 21,
channel_mask: [21],
network_key: {
key: "01030507090b0d0f00020406080a0c0d",
sequence_number: 0,
frame_counter: 16754,
},
devices: [
{
nwk_address: "ddf6",
ieee_address: "00124b002226ef87",
},
{
nwk_address: "c2dc",
ieee_address: "04cf8cdf3c79455f",
link_key: {
key: "0e768569dd935d8e7302e74e7629f13f",
rx_counter: 0,
tx_counter: 275,
},
},
{
nwk_address: "740a",
ieee_address: "680ae2fffeae5647",
link_key: {
key: "7c079d02aae015facd7ae9608d4baf56",
rx_counter: 0,
tx_counter: 275,
},
},
{
nwk_address: "19fa",
ieee_address: "00158d00024fa79b",
link_key: {
key: "cea550908aa1529ee90eea3c3bdc26fc",
rx_counter: 0,
tx_counter: 44,
},
},
{
nwk_address: "6182",
ieee_address: "00158d00024f4518",
link_key: {
key: "267e1e31fcd8171f8acf63459effbca5",
rx_counter: 0,
tx_counter: 44,
},
},
{
nwk_address: "4285",
ieee_address: "00158d00024f810d",
link_key: {
key: "55ba1e31fcd8171f9f0b63459effbca5",
rx_counter: 0,
tx_counter: 44,
},
},
],
};
const legacyBackup = {
adapterType: "zStack",
time: "Thu, 04 Mar 2021 10:55:12 GMT",
meta: {
product: 2,
},
data: {
ZCD_NV_EXTADDR: {
id: 1,
offset: 0,
osal: true,
product: -1,
value: [167, 11, 216, 9, 0, 75, 18, 0],
len: 8,
},
ZCD_NV_NIB: {
id: 33,
offset: 0,
osal: true,
product: -1,
value: [
145, 5, 2, 16, 20, 16, 0, 20, 0, 0, 0, 1, 5, 1, 143, 7, 0, 2, 5, 30, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 0, 8, 0, 0, 32, 0,
15, 15, 4, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 119, 159, 214, 9, 0, 75, 18, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 3, 0, 1, 120, 10, 1, 0, 0, 146, 235, 0,
],
len: 110,
},
ZCD_NV_PANID: {
id: 131,
offset: 0,
osal: true,
product: -1,
value: [123, 0],
len: 2,
},
ZCD_NV_EXTENDED_PAN_ID: {
id: 45,
offset: 0,
osal: true,
product: -1,
value: [221, 221, 221, 221, 221, 221, 221, 221],
len: 8,
},
ZCD_NV_NWK_ACTIVE_KEY_INFO: {
id: 58,
offset: 0,
osal: true,
product: -1,
value: [0, 1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
len: 17,
},
ZCD_NV_NWK_ALTERN_KEY_INFO: {
id: 59,
offset: 0,
osal: true,
product: -1,
value: [0, 1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
len: 17,
},
ZCD_NV_APS_USE_EXT_PANID: {
id: 71,
offset: 0,
osal: true,
product: -1,
value: [0, 0, 0, 0, 0, 0, 0, 0],
len: 8,
},
ZCD_NV_PRECFGKEY: {
id: 98,
offset: 0,
osal: true,
product: -1,
value: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
len: 16,
},
ZCD_NV_PRECFGKEY_ENABLE: {
id: 99,
offset: 0,
osal: true,
product: -1,
value: [0],
len: 1,
},
ZCD_NV_CHANLIST: {
id: 132,
offset: 0,
osal: true,
product: -1,
value: [0, 20, 0, 0],
len: 4,
},
ZCD_NV_LEGACY_TCLK_TABLE_START: {
id: 273,
product: 2,
offset: 0,
osal: true,
value: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0],
len: 19,
},
ZCD_NV_LEGACY_NWK_SEC_MATERIAL_TABLE_START: {
id: 117,
product: 2,
offset: 0,
osal: true,
value: [83, 144, 14, 0, 134, 114, 56, 25, 0, 75, 18, 0],
len: 12,
},
},
};
class ZnpRequestMockBuilder {
// biome-ignore lint/suspicious/noExplicitAny: API
public responders: {subsystem: Subsystem; command: string; exec: (payload: any, handler?: ZnpRequestMockBuilder) => any}[] = [];
public nvItems: {id: NvItemsIds; value?: Buffer}[] = [];
public nvExtendedItems: {sysId: NvSystemIds; id: NvItemsIds; subId: number; value?: Buffer}[] = [];
constructor() {
const handleOsalNvRead = (payload, handler) => {
if (payload.offset !== undefined && payload.offset !== 0) {
throw new Error("osalNvLength offset not supported");
}
const item = handler.nvItems.find((e) => e.id === payload.id);
return {payload: {status: item?.value ? 0 : 1, value: item?.value ? item.value : undefined}};
};
this.handle(Subsystem.SYS, "osalNvRead", handleOsalNvRead);
this.handle(Subsystem.SYS, "osalNvReadExt", handleOsalNvRead);
const handleOsalNvWrite = (payload, handler) => {
if (payload.offset !== undefined && payload.offset !== 0) {
throw new Error("osalNvLength offset not supported");
}
const item = handler.nvItems.find((e) => e.id === payload.id);
if (item) {
item.value = payload.value;
return {payload: {status: 0}};
}
return {payload: {status: 1}};
};
this.handle(Subsystem.SYS, "osalNvWrite", handleOsalNvWrite);
this.handle(Subsystem.SYS, "osalNvWriteExt", handleOsalNvWrite);
this.handle(Subsystem.SYS, "osalNvItemInit", (payload, handler) => {
let item = handler.nvItems.find((e) => e.id === payload.id);
if (item) {
if (item.value && item.value.length !== payload.len) {
return {payload: {status: 0x0a}};
}
return {payload: {status: 0x00}};
}
item = {
id: payload.id,
value: payload.initvalue || null,
};
handler.nvItems.push(item);
return {payload: {status: 0x09}};
});
this.handle(Subsystem.SYS, "osalNvLength", (payload, handler) => {
if (payload.offset !== undefined && payload.offset !== 0) {
throw new Error("osalNvLength offset not supported");
}
const item = handler.nvItems.find((e) => e.id === payload.id);
return {payload: {length: item?.value ? item.value.length : 0}};
});
this.handle(Subsystem.SYS, "osalNvDelete", (payload, handler) => {
const item = handler.nvItems.find((e) => e.id === payload.id);
if (item) {
if (item.value && item.value.length !== payload.len) {
return {payload: {status: 0x0a}};
}
const itemIndex = handler.nvItems.indexOf(item);
handler.nvItems.splice(itemIndex, 1);
return {payload: {status: 0x00}};
}
return {payload: {status: 0x09}};
});
this.handle(Subsystem.SYS, "nvRead", (payload, handler: ZnpRequestMockBuilder) => {
if (payload.offset !== undefined && payload.offset !== 0) {
throw new Error("nvRead offset not supported");
}
const item = handler.nvExtendedItems.find((e) => e.sysId === payload.sysid && e.id === payload.itemid && e.subId === payload.subid);
return {
payload: {
status: item?.value ? 0 : 1,
value: item?.value ? item.value : undefined,
len: item?.value?.length || undefined,
},
};
});
this.handle(Subsystem.SYS, "nvWrite", (payload, handler: ZnpRequestMockBuilder) => {
if (payload.offset !== undefined && payload.offset !== 0) {
throw new Error("nwWrite offset not supported");
}
const item = handler.nvExtendedItems.find((e) => e.sysId === payload.sysid && e.id === payload.itemid && e.subId === payload.subid);
if (item) {
item.value = payload.value;
return {payload: {status: 0}};
}
return {payload: {status: 1}};
});
this.handle(Subsystem.SYS, "nvCreate", (payload, handler: ZnpRequestMockBuilder) => {
let item = handler.nvExtendedItems.find((e) => e.sysId === payload.sysid && e.id === payload.itemid && e.subId === payload.subid);
if (item) {
if (item.value && item.value.length !== payload.len) {
return {payload: {status: 0x0a}};
}
return {payload: {status: 0x00}};
}
item = {
sysId: payload.sysid,
id: payload.itemid,
subId: payload.subid,
value: null,
};
handler.nvExtendedItems.push(item);
return {payload: {status: 0x09}};
});
this.handle(Subsystem.SYS, "nvLength", (payload, handler) => {
if (payload.offset !== undefined && payload.offset !== 0) {
throw new Error("nvLength offset not supported");
}
const item = handler.nvExtendedItems.find((e) => e.sysId === payload.sysid && e.id === payload.itemid && e.subId === payload.subid);
return {payload: {len: item?.value ? item.value.length : 0}};
});
this.handle(Subsystem.SYS, "nvDelete", (payload, handler) => {
const item = handler.nvExtendedItems.find((e) => e.sysId === payload.sysid && e.id === payload.itemid && e.subId === payload.subid);
if (item) {
if (item.value && item.value.length !== payload.len) {
return {payload: {status: 0x0a}};
}
const itemIndex = handler.nvItems.indexOf(item);
handler.nvItems.splice(itemIndex, 1);
return {payload: {status: 0x00}};
}
return {payload: {status: 0x09}};
});
}
// biome-ignore lint/suspicious/noExplicitAny: API
public handle(subsystem: Subsystem, command: string, exec?: (payload: any, handler?: ZnpRequestMockBuilder) => any) {
const index = this.responders.findIndex((r) => r.subsystem === subsystem && r.command === command);
if (index > -1) {
this.responders.splice(index, 1);
}
this.responders.push({subsystem, command, exec: exec || (() => ({}))});
return this;
}
public nv(id: NvItemsIds, value?: Buffer) {
const index = this.nvItems.findIndex((e) => e.id === id);
if (index > -1) {
this.nvItems.splice(index, 1);
}
if (value) {
this.nvItems.push({id, value: value || null});
}
return this;
}
public nvExtended(sysId: NvSystemIds, id: NvItemsIds, subId: number, value?: Buffer) {
const index = this.nvExtendedItems.findIndex((e) => e.sysId === sysId && e.id === id && e.subId === subId);
if (index > -1) {
this.nvExtendedItems.splice(index, 1);
}
if (value) {
this.nvExtendedItems.push({sysId, id, subId, value: value || null});
}
return this;
}
// biome-ignore lint/suspicious/noExplicitAny: API
public execute(message: {subsystem: Subsystem; command: string; payload: any}) {
const responder = this.responders.find((r) => r.subsystem === message.subsystem && r.command === message.command);
if (!responder) {
const msg = `Not implemented - ${Subsystem[message.subsystem]} - ${message.command} - ${JSON.stringify(message.payload)}`;
console.log(msg);
throw new Error(msg);
}
const response = responder.exec(message.payload, this);
return response;
}
public clone(): ZnpRequestMockBuilder {
const newBuilder = new ZnpRequestMockBuilder();
newBuilder.responders = this.responders.map((responder) => ({...responder}));
newBuilder.nvItems = this.nvItems.map((item) => ({...item, value: Buffer.from(item.value)}));
newBuilder.nvExtendedItems = this.nvExtendedItems.map((item) => ({...item, value: Buffer.from(item.value)}));
return newBuilder;
}
}
const baseZnpRequestMock = new ZnpRequestMockBuilder()
.handle(Subsystem.SYS, "version", (payload) => (equals(payload, {}) ? {payload: {product: ZnpVersion.ZStack30x, revision: 20201026}} : undefined))
.handle(Subsystem.SYS, "ping", () => ({}))
.handle(Subsystem.SYS, "resetReq", () => ({}))
.handle(Subsystem.SYS, "getExtAddr", () => ({payload: {extaddress: "0x00124b0009d69f77"}}))
.handle(Subsystem.SYS, "stackTune", () => ({}))
.handle(Subsystem.ZDO, "extFindGroup", () => ({payload: {status: 0}}))
.handle(Subsystem.ZDO, "extAddGroup", () => ({payload: {status: 0}}))
.handle(Subsystem.UTIL, "getDeviceInfo", () => ({payload: {devicestate: 0x00, ieeeaddr: "0x00124b0009d80ba7"}}))
.handle(Subsystem.ZDO, "activeEpReq", () => ({}))
.handle(Subsystem.ZDO, "simpleDescReq", () => ({}))
.handle(Subsystem.ZDO, "mgmtPermitJoinReq", () => ({}))
.handle(Subsystem.ZDO, "nodeDescReq", () => ({}))
.handle(Subsystem.ZDO, "bindReq", () => ({}))
.handle(Subsystem.ZDO, "unbindReq", () => ({}))
.handle(Subsystem.ZDO, "mgmtLeaveReq", () => ({}))
.handle(Subsystem.ZDO, "mgmtLqiReq", () => ({}))
.handle(Subsystem.ZDO, "mgmtRtgReq", () => ({}))
.handle(Subsystem.ZDO, "mgmtNwkUpdateReq", () => ({}))
.handle(Subsystem.AF, "interPanCtl", () => ({}))
.handle(Subsystem.ZDO, "extRouteDisc", () => ({}))
.handle(Subsystem.ZDO, "nwkAddrReq", () => ({}))
.handle(Subsystem.UTIL, "assocRemove", () => ({payload: {}}))
.handle(Subsystem.UTIL, "assocGetWithAddress", () => ({payload: {noderelation: assocGetWithAddressNodeRelation}}))
.handle(Subsystem.UTIL, "assocAdd", () => ({payload: {}}))
.handle(Subsystem.UTIL, "ledControl", () => ({}))
.handle(Subsystem.APP_CNF, "bdbAddInstallCode", () => ({}))
.handle(Subsystem.AF, "register", () => ({}))
.handle(Subsystem.AF, "dataRequest", () => {
if (dataRequestCode !== 0) {
throw new Error(`Data request failed with code '${dataRequestCode}'`);
}
return {};
})
.handle(Subsystem.AF, "dataRequestExt", () => {
if (dataRequestExtCode !== 0) {
throw new Error(`Data request failed with code '${dataRequestExtCode}'`);
}
return {};
})
.handle(Subsystem.SYS, "resetReq", () => ({}))
.handle(Subsystem.APP_CNF, "bdbSetChannel", () => ({}))
.handle(Subsystem.APP_CNF, "bdbStartCommissioning", (_, handler) => {
const nibIndex = handler.nvItems.findIndex((e) => e.id === NvItemsIds.NIB);
if (nibIndex > -1) {
handler.nvItems.splice(nibIndex, 1);
}
handler.nvItems.push({
id: NvItemsIds.NIB,
value: Buffer.from(
"fb050279147900640000000105018f000700020d1e0000001500000000000000000000007b000800000020000f0f0400010000000100000000779fd609004b1200010000000000000000000000000000000000000000000000000000000000000000000000003c0c0001780a0100000006020000",
"hex",
),
});
return {};
})
.handle(Subsystem.ZDO, "extNwkInfo", (_, handler) => {
const nib = Structs.nib(handler.nvItems.find((item) => item.id === NvItemsIds.NIB).value);
return {payload: {panid: nib.nwkPanId, extendedpanid: `0x${nib.extendedPANID.toString("hex")}`, channel: nib.nwkLogicalChannel}};
})
.handle(Subsystem.ZDO, "startupFromApp", () => ({}))
.nv(NvItemsIds.CHANLIST, Buffer.from([0, 8, 0, 0]))
.nv(NvItemsIds.PRECFGKEY, Buffer.alloc(16, 0))
.nv(NvItemsIds.PRECFGKEYS_ENABLE, Buffer.from([0]))
.nv(NvItemsIds.NWKKEY, Buffer.alloc(24, 0))
.nv(NvItemsIds.NWK_ACTIVE_KEY_INFO, Buffer.from("000000000000000000000000000000000000", "hex"))
.nv(NvItemsIds.NWK_ALTERN_KEY_INFO, Buffer.from("000000000000000000000000000000000000", "hex"))
.nv(NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK3, Buffer.from([0x00]))
.nv(
NvItemsIds.NIB,
Buffer.from(
"fb050279147900640000000105018f000700020d1e0000001500000000000000000000000bcd0800000020000f0f0400010000000100000000779fd609004b1200010000000000000000000000000000000000000000000000000000000000000000000000003c0c0001780a0100000006020000",
"hex",
),
);
const empty3UnalignedRequestMock = baseZnpRequestMock
.clone()
.handle(Subsystem.APP_CNF, "bdbStartCommissioning", (_, handler) => {
const nibIndex = handler.nvItems.findIndex((e) => e.id === NvItemsIds.NIB);
if (nibIndex > -1) {
handler.nvItems.splice(nibIndex, 1);
}
handler.nvItems.push({
id: NvItemsIds.NIB,
value: Buffer.from(
"fb050279147900640000000105018f0700020d1e000015000000000000000000007b0008000020000f0f0400010000000100000000779fd609004b1200010000000000000000000000000000000000000000000000000000000000000000000000003c0c0001780a010000060200",
"hex",
),
});
return {};
})
.nv(NvItemsIds.NWKKEY, Buffer.alloc(21, 0))
.nv(NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK3, Buffer.from([0x00]))
.nv(
NvItemsIds.NIB,
Buffer.from(
"fb050279147900640000000105018f0700020d1e00001500000000000000000000ffff08000020000f0f0400010000000100000000779fd609004b1200010000000000000000000000000000000000000000000000000000000000000000000000003c0c0001780a010000060200",
"hex",
),
)
.nv(
NvItemsIds.ADDRMGR,
Buffer.from(
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"hex",
),
)
.nv(
NvItemsIds.APS_LINK_KEY_TABLE,
Buffer.from(
"0000feff000000feff000000feff000000feff000000feff000000feff000000feff000000feff000000feff000000feff000000feff000000feff000000feff000000feff000000feff000000feff000000",
"hex",
),
);
for (let i = 0; i < 4; i++) {
empty3UnalignedRequestMock.nv(NvItemsIds.LEGACY_NWK_SEC_MATERIAL_TABLE_START + i, Buffer.from("000000000000000000000000", "hex"));
}
for (let i = 0; i < 16; i++) {
empty3UnalignedRequestMock.nv(NvItemsIds.LEGACY_TCLK_TABLE_START + i, Buffer.from("00000000000000000000000000000000000000", "hex"));
}
for (let i = 0; i < 16; i++) {
empty3UnalignedRequestMock.nv(NvItemsIds.APS_LINK_KEY_DATA_START + i, Buffer.from("000000000000000000000000000000000000000000000000", "hex"));
}
const empty3AlignedRequestMock = baseZnpRequestMock
.clone()
.nv(NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK3, Buffer.from([0x00]))
.nv(
NvItemsIds.NIB,
Buffer.from(
"fb050279147900640000000105018f000700020d1e000000150000000000000000000000ffff0800000020000f0f0400010000000100000000779fd609004b1200010000000000000000000000000000000000000000000000000000000000000000000000003c0c0001780a0100000006020000",
"hex",
),
)
.nv(
NvItemsIds.ADDRMGR,
Buffer.from(
"00ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff00000000000000000000",
"hex",
),
)
.nv(
NvItemsIds.APS_LINK_KEY_TABLE,
Buffer.from(
"0000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000",
"hex",
),
);
for (let i = 0; i < 4; i++) {
empty3AlignedRequestMock.nv(NvItemsIds.LEGACY_NWK_SEC_MATERIAL_TABLE_START + i, Buffer.from("000000000000000000000000", "hex"));
}
for (let i = 0; i < 16; i++) {
empty3AlignedRequestMock.nv(NvItemsIds.LEGACY_TCLK_TABLE_START + i, Buffer.from("0000000000000000000000000000000000000000", "hex"));
}
for (let i = 0; i < 16; i++) {
empty3AlignedRequestMock.nv(NvItemsIds.APS_LINK_KEY_DATA_START + i, Buffer.from("000000000000000000000000000000000000000000000000", "hex"));
}
const commissioned3AlignedRequestMock = empty3AlignedRequestMock
.clone()
.nv(NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK3, Buffer.from([0x55]))
.nv(NvItemsIds.PRECFGKEY, Buffer.from("01030507090b0d0f00020406080a0c0d", "hex"))
.nv(NvItemsIds.NWK_ACTIVE_KEY_INFO, Buffer.from("0001030507090b0d0f00020406080a0c0d00", "hex"))
.nv(NvItemsIds.NWK_ALTERN_KEY_INFO, Buffer.from("0001030507090b0d0f00020406080a0c0d00", "hex"))
.nv(
NvItemsIds.NIB,
Buffer.from(
"fb050279147900640000000105018f000700020d1e0000001500000000000000000000007b000800000020000f0f0400010000000100000000779fd609004b1200010000000000000000000000000000000000000000000000000000000000000000000000003c0c0001780a0100000006020000",
"hex",
),
)
.nv(
NvItemsIds.ADDRMGR,
Buffer.from(
"01ff4f3a080000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff0000000000000000000000ff00000000000000000000",
"hex",
),
);
const commissioned3AlignedConfigMistmachRequestMock = commissioned3AlignedRequestMock
.clone()
.nv(NvItemsIds.NWK_ACTIVE_KEY_INFO, Buffer.from("0001030507090b0d0f00020406080a0c0d00", "hex"))
.nv(NvItemsIds.NWK_ALTERN_KEY_INFO, Buffer.from("0001030507090b0d0f00020406080a0c0d00", "hex"))
.nv(
NvItemsIds.NIB,
Buffer.from(
"fb050279147900640000000105018f000700020d1e0000001500000000000000000000007e000800000020000f0f0400010000000100000000779fd609004b1200010000000000000000000000000000000000000000000000000000000000000000000000003c0c0001780a0100000006020000",
"hex",
),
);
const empty3x0AlignedRequestMock = baseZnpRequestMock
.clone()
.handle(Subsystem.SYS, "version", (payload) => (equals(payload, {}) ? {payload: {product: ZnpVersion.ZStack3x0, revision: 20210430}} : undefined))
.nv(NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK3, Buffer.from([0x00]))
.nv(
NvItemsIds.NIB,
Buffer.from(
"fb050279147900640000000105018f000700020d1e000000150000000000000000000000ffff0800000020000f0f0400010000000100000000779fd609004b1200010000000000000000000000000000000000000000000000000000000000000000000000003c0c0001780a0100000006020000",
"hex",
),
)
.nv(
NvItemsIds.APS_LINK_KEY_TABLE,
Buffer.from(
"0000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000feff00000000",
"hex",
),
);
for (let i = 0; i < 16; i++) {
empty3x0AlignedRequestMock.nvExtended(NvSystemIds.ZSTACK, NvItemsIds.ZCD_NV_EX_ADDRMGR, i, Buffer.from("00ff00000000000000000000", "hex"));
}
for (let i = 0; i < 4; i++) {
empty3x0AlignedRequestMock.nvExtended(
NvSystemIds.ZSTACK,
NvItemsIds.EX_NWK_SEC_MATERIAL_TABLE,
i,
Buffer.from("000000000000000000000000", "hex"),
);
}
for (let i = 0; i < 16; i++) {
empty3x0AlignedRequestMock.nvExtended(
NvSystemIds.ZSTACK,
NvItemsIds.EX_TCLK_TABLE,
i,
Buffer.from("0000000000000000000000000000000000000000", "hex"),
);
}
for (let i = 0; i < 16; i++) {
empty3x0AlignedRequestMock.nvExtended(
NvSystemIds.ZSTACK,
NvItemsIds.ZCD_NV_EX_APS_KEY_DATA_TABLE,
i,
Buffer.from("000000000000000000000000000000000000000000000000", "hex"),
);
}
const commissioned3x0AlignedRequestMock = empty3x0AlignedRequestMock
.clone()
.nv(NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK3, Buffer.from([0x55]))
.nv(NvItemsIds.PRECFGKEY, Buffer.from("01030507090b0d0f00020406080a0c0d", "hex"))
.nv(NvItemsIds.NWK_ACTIVE_KEY_INFO, Buffer.from("0001030507090b0d0f00020406080a0c0d00", "hex"))
.nv(NvItemsIds.NWK_ALTERN_KEY_INFO, Buffer.from("0001030507090b0d0f00020406080a0c0d00", "hex"))
.nv(
NvItemsIds.NIB,
Buffer.from(
"fb050279147900640000000105018f000700020d1e0000001500000000000000000000007b000800000020000f0f0400010000000100000000779fd609004b1200010000000000000000000000000000000000000000000000000000000000000000000000003c0c0001780a0100000006020000",
"hex",
),
)
.nvExtended(NvSystemIds.ZSTACK, NvItemsIds.ZCD_NV_EX_ADDRMGR, 0, Buffer.from("01ff4f3a0800000000000000", "hex"));
const empty12UnalignedRequestMock = baseZnpRequestMock
.clone()
.handle(Subsystem.SYS, "version", (payload) => (equals(payload, {}) ? {payload: {product: ZnpVersion.ZStack12}} : undefined))
.handle(Subsystem.SAPI, "readConfiguration", (payload, handler) => {
if (payload.configid !== NvItemsIds.PRECFGKEY) {
throw new Error("Only pre-configured key should be read/written using SAPI layer");
}
const item = handler.nvItems.find((item) => item.id === payload.configid);
if (item) {
return {payload: {status: 0, configid: item.id, len: item.value?.length || 0, value: item.value}};
}
return {payload: {status: 1}};
})
.handle(Subsystem.SAPI, "writeConfiguration", (payload, handler) => {
if (payload.configid !== NvItemsIds.PRECFGKEY) {
throw new Error("Only pre-configured key should be read/written using SAPI layer");
}
const item = handler.nvItems.find((item) => item.id === payload.configid);
if (item) {
item.value = payload.value;
} else {
handler.nvItems.push({id: payload.configid, value: payload.value});
}
handler.nv(
NvItemsIds.NIB,
Buffer.from(
"fb050279147900640000000105018f0700020d1e000015000000000000000000007b0008000020000f0f0400010000000100000000779fd609004b1200010000000000000000000000000000000000000000000000000000000000000000000000003c0c0001780a010000060200",
"hex",
),
);
return {payload: {status: 0}};
})
.nv(NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK1, Buffer.from([0x00]));
const commissioned12UnalignedRequestMock = empty12UnalignedRequestMock
.clone()
.nv(NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK1, Buffer.from([0x55]))
.nv(NvItemsIds.PRECFGKEY, Buffer.from("01030507090b0d0f00020406080a0c0d", "hex"))
.nv(
NvItemsIds.NIB,
Buffer.from(
"fb050279147900640000000105018f0700020d1e000015000000000000000000007b0008000020000f0f0400010000000100000000779fd609004b1200010000000000000000000000000000000000000000000000000000000000000000000000003c0c0001780a010000060200",
"hex",
),
);
const commissioned12UnalignedMismatchRequestMock = commissioned12UnalignedRequestMock
.clone()
.nv(NvItemsIds.PRECFGKEY, Buffer.from("aabb0507090b0d0f00020406080a0c0d", "hex"));
const mockZnpRequest = vi
.fn()
.mockReturnValue(new Promise((resolve) => resolve({payload: {}})))
.mockImplementation(
// biome-ignore lint/suspicious/noExplicitAny: API
(subsystem: Subsystem, command: string, payload: any, _expectedStatus: ZnpCommandStatus) =>
new Promise((resolve) => resolve(baseZnpRequestMock.execute({subsystem, command, payload}))),
);
const mockZnpRequestZdo = vi.fn();
const mockZnpWaitFor = vi.fn();
const mockZnpOpen = vi.fn();
const mockZnpClose = vi.fn();
const mockQueueExecute = vi.fn().mockImplementation(async (func) => await func());
const mocks = [mockZnpOpen, mockZnpRequest, mockZnpClose];
const mockZnpRequestWith = (builder: ZnpRequestMockBuilder) => {
builder = builder.clone();
mockZnpRequest.mockImplementation(
// biome-ignore lint/suspicious/noExplicitAny: API
(subsystem: Subsystem, command: string, payload: any, _expectedStatus: ZnpCommandStatus) =>
new Promise((resolve) => resolve(builder.execute({subsystem, command, payload}))),
);
};
const mockZnpWaitForDefault = () => {
mockZnpWaitFor.mockImplementation((type, subsystem, command, target, transid, state, timeout) => {
const missing = () => {
const msg = `Not implemented - ${Type[type]} - ${Subsystem[subsystem]} - ${command} - ${target} - ${transid} - ${state} - ${timeout}`;
console.log(msg);
throw new Error(msg);
};
if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "activeEpRsp") {
return waitForResult(
mockZdoZpiObject<ActiveEndpointsResponse>("activeEpRsp", target, [
Zdo.Status.SUCCESS,
{
nwkAddress: 0,
endpointList: [],
},
]),
);
}
if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "mgmtPermitJoinRsp") {
return waitForResult(mockZdoZpiObject("mgmtPermitJoinRsp", target, [Zdo.Status.SUCCESS, undefined]));
}
if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "stateChangeInd") {
return waitForResult({payload: {state: 9}});
}
missing();
});
};
const mockZnpWaitForStateChangeIndTimeout = () => {
mockZnpWaitFor.mockImplementation((type, subsystem, command, target, transid, state, timeout) => {
const missing = () => {
const msg = `Not implemented - ${Type[type]} - ${Subsystem[subsystem]} - ${command} - ${target} - ${transid} - ${state} - ${timeout}`;
console.log(msg);
throw new Error(msg);
};
if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "activeEpRsp") {
return waitForResult(
mockZdoZpiObject<ActiveEndpointsResponse>("activeEpRsp", target, [
Zdo.Status.SUCCESS,
{
nwkAddress: 0,
endpointList: [],
},
]),
);
}
if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "stateChangeInd") {
return;
}
missing();
});
};
let bindStatusResponse = 0;
const mockZdoZpiObject = <T>(commandName: string, srcaddr: number | undefined, payload: [Zdo.Status, T | undefined]): ZpiObject => {
const subsystem = Subsystem.ZDO;
const command = Definition[subsystem].find((c) => c.name === commandName)!;
return {
type: Type.AREQ,
subsystem,
command,
payload: {srcaddr, zdo: payload},
};
};
const mockZpiObject = (type: Type, subsystem: Subsystem, commandName: string, payload: {[s: string]: unknown}) => {
const command = Definition[subsystem].find((c) => c.name === commandName);
return {type, subsystem, payload, command};
};
const basicMocks = () => {
mockZnpRequestWith(commissioned3x0AlignedRequestMock);
mockZnpWaitFor.mockImplementation((type, subsystem, command, target, transid, state, timeout) => {
const missing = () => {
const msg = `Not implemented - ${Type[type]} - ${Subsystem[subsystem]} - ${command} - ${target} - ${transid} - ${state} - ${timeout}`;
console.log(msg);
throw new Error(msg);
};
if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "activeEpRsp") {
return waitForResult(
mockZdoZpiObject<ActiveEndpointsResponse>("activeEpRsp", target, [
Zdo.Status.SUCCESS,
{
nwkAddress: 0,
endpointList: [1, 2, 3, 4, 5, 6, 8, 10, 11, 110, 12, 13, 47, 242],
},
]),
);
}
if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "stateChangeInd") {
return waitForResult({payload: {}});
}
if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "mgmtPermitJoinRsp") {
return waitForResult(mockZdoZpiObject("mgmtPermitJoinRsp", target, [Zdo.Status.SUCCESS, undefined]));
}
if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "simpleDescRsp") {
let responsePayload: SimpleDescriptorResponse;
if (simpleDescriptorEndpoint === 1) {
responsePayload = {
length: 1, // bogus
endpoint: 1,
profileId: 123,
deviceId: 5,
inClusterList: [1],
outClusterList: [2],
nwkAddress: target,
deviceVersion: 0,
};
} else if (simpleDescriptorEndpoint === 99) {
responsePayload = {
length: 1, // bogus
endpoint: 99,
profileId: 123,
deviceId: 5,
inClusterList: [1],
outClusterList: [2],
nwkAddress: target,
deviceVersion: 0,
};
} else {
responsePayload = {
length: 1, // bogus
endpoint: simpleDescriptorEndpoint,
profileId: 124,
deviceId: 7,
inClusterList: [8],
outClusterList: [9],
nwkAddress: target,
deviceVersion: 0,
};
}
return waitForResult(mockZdoZpiObject<SimpleDescriptorResponse>("simpleDescRsp", target, [Zdo.Status.SUCCESS, responsePayload]));
}
if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "nodeDescRsp") {
if (nodeDescRspErrorOnce) {
nodeDescRspErrorOnce = false;
return {
start: () => {
return {
promise: new Promise((_resolve, reject) => {
reject("timeout after xx");
}),
};
},
ID: 89,
};
}
return waitForResult(
mockZdoZpiObject<NodeDescriptorResponse>("nodeDescRsp", target, [
Zdo.Status.SUCCESS,
{
manufacturerCode: target * 2,
apsFlags: 0,
capabilities: DUMMY_NODE_DESC_RSP_CAPABILITIES,
deprecated1: 0,
fragmentationSupported: true,
frequencyBand: 0,
logicalType: target - 1,
maxBufSize: 0,
maxIncTxSize: 0,
maxOutTxSize: 0,
nwkAddress: target,
serverMask: {
backupTrustCenter: 0,
deprecated1: 0,
deprecated2: 0,
deprecated3: 0,
deprecated4: 0,
networkManager: 0,
primaryTrustCenter: 0,
reserved1: 0,
reserved2: 0,
stackComplianceRevision: 0,
},
tlvs: [],
},
]),
);
}
if (type === Type.AREQ && subsystem === Subsystem.AF && command === "dataConfirm") {
const status = dataConfirmCode;
if (dataConfirmCodeReset) {
dataConfirmCode = 0;
}
if (status === 9999) {
return {
start: () => {
return {
promise: new Promise((_resolve, reject) => {
reject("timeout after xx");
}),
};
},
ID: 99,
};
}
return waitForResult({payload: {status}}, 99);
}
if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "mgmtLqiRsp" && target === 203) {
const defaults = {deviceType: 0, extendedPanId: [0], permitJoining: 0, reserved1: 0, reserved2: 0, rxOnWhenIdle: 0};
if (lastStartIndex === 0) {
lastStartIndex += 2;
return waitForResult(
mockZdoZpiObject<LQITableResponse>("mgmtLqiRsp", target, [
Zdo.Status.SUCCESS,
{
neighborTableEntries: 5,
startIndex: 0,
entryList: [
{lqi: 10, nwkAddress: 2, eui64: "0x3", relationship: 3, depth: 1, ...defaults},
{lqi: 15, nwkAddress: 3, eui64: "0x4", relationship: 2, depth: 5, ...defaults},
],
},
]),
);
}
if (lastStartIndex === 2) {
lastStartIndex += 2;
return waitForResult(
mockZdoZpiObject<LQITableResponse>("mgmtLqiRsp", target, [
Zdo.Status.SUCCESS,
{
neighborTableEntries: 5,
startIndex: 0,
entryList: [
{lqi: 10, nwkAddress: 5, eui64: "0x6", relationship: 3, depth: 1, ...defaults},
{lqi: 15, nwkAddress: 7, eui64: "0x8", relationship: 2, depth: 5, ...defaults},
],
},
]),
);
}
if (lastStartIndex === 4) {
return waitForResult(
mockZdoZpiObject<LQITableResponse>("mgmtLqiRsp", target, [
Zdo.Status.SUCCESS,
{
neighborTableEntries: 5,
startIndex: 0,
entryList: [{lqi: 10, nwkAddress: 9, eui64: "0x10", relationship: 3, depth: 1, ...defaults}],
},
]),
);
}
} else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "mgmtLqiRsp" && target === 204) {
return waitForResult(mockZdoZpiObject<LQITableResponse>("mgmtLqiRsp", target, [Zdo.Status.NOT_AUTHORIZED, undefined]));
} else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === "mgmtRtgRsp" && target === 205) {
const defaultEntryList = {
manyToOne: 0,
memoryConstrained: 0,
reserved1: 0,
routeRecordRequired: 0,
status: Zdo.RoutingTableStatus[0] as keyof typeof Zdo.RoutingTableStatus,
};
if (lastStartIndex === 0) {
lastStartIndex += 2;
return waitForResult(
mockZdoZpiObject<RoutingTableResponse>("mgmtRtgRsp", target, [
Zdo.Status.SUCCESS,
{
startIndex: 0,
routingTableEntries: 5,
entryList: [
{destinationAddress: 10, nextHopAddress: 3, ...defaultEntryList},
{destinationAddress: 11, nextHopAddress: 3, ...defaultEntryList},
],
},
]),
);
}
if (lastStartIndex === 2) {
lastStartIndex += 2;
return waitForResult(
mockZdoZpiObject<RoutingTableResponse>("mgmtRtgRsp", target, [
Zdo.Status.SUCCESS,
{
startIndex: 0,
routingTableEntries: 5,
entryList: [
{destinationAddress: 12, nextHopAddress: 3, ...defaultEntryList},
{destinationAddress: 13, nextHopAddress: 3, ...defaultEntryList},
],
},
]),
);
}
if (lastStartIndex === 4) {
return waitForResult(
mockZdoZpiO