UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

1,262 lines (1,187 loc) 162 kB
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