UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

1,232 lines (1,094 loc) 148 kB
import {existsSync, mkdirSync, unlinkSync, writeFileSync} from "node:fs"; import path from "node:path"; import {EventEmitter} from "node:stream"; import type {TsType} from "../../../src/adapter"; import { DEFAULT_APS_OPTIONS, DEFAULT_STACK_CONFIG, EmberAdapter, type LinkKeyBackupData, type NetworkCache, } from "../../../src/adapter/ember/adapter/emberAdapter"; import {FIXED_ENDPOINTS} from "../../../src/adapter/ember/adapter/endpoints"; import {OneWaitressEvents} from "../../../src/adapter/ember/adapter/oneWaitress"; import {EMBER_LOW_RAM_CONCENTRATOR, SECURITY_LEVEL_Z3} from "../../../src/adapter/ember/consts"; import { EmberApsOption, EmberDeviceUpdate, EmberIncomingMessageType, EmberJoinDecision, EmberJoinMethod, EmberKeyStructBitmask, EmberNetworkStatus, EmberNodeType, EmberOutgoingMessageType, EmberVersionType, EzspStatus, IEEE802154CcaMode, SLStatus, SecManDerivedKeyType, SecManFlag, SecManKeyType, } from "../../../src/adapter/ember/enums"; import {EZSP_MIN_PROTOCOL_VERSION, EZSP_PROTOCOL_VERSION, EZSP_STACK_TYPE_MESH} from "../../../src/adapter/ember/ezsp/consts"; import {EzspConfigId, EzspDecisionBitmask, EzspEndpointFlag, EzspPolicyId, EzspValueId} from "../../../src/adapter/ember/ezsp/enums"; import type {EmberEzspEventMap} from "../../../src/adapter/ember/ezsp/ezsp"; import {EzspError} from "../../../src/adapter/ember/ezspError"; import type { EmberApsFrame, EmberMulticastTableEntry, EmberNetworkInitStruct, EmberNetworkParameters, EmberVersion, SecManAPSKeyMetadata, SecManContext, SecManKey, SecManNetworkKeyInfo, } from "../../../src/adapter/ember/types"; import {lowHighBytes} from "../../../src/adapter/ember/utils/math"; import type {DeviceJoinedPayload, DeviceLeavePayload, ZclPayload} from "../../../src/adapter/events"; import type {AdapterOptions, NetworkOptions, SerialPortOptions} from "../../../src/adapter/tstype"; import type {Backup} from "../../../src/models/backup"; import type {UnifiedBackupStorage} from "../../../src/models/backup-storage-unified"; import {logger} from "../../../src/utils/logger"; import * as ZSpec from "../../../src/zspec"; import type {Eui64, NodeId, PanId} from "../../../src/zspec/tstypes"; import * as Zcl from "../../../src/zspec/zcl"; import * as Zdo from "../../../src/zspec/zdo"; import type * as ZdoTypes from "../../../src/zspec/zdo/definition/tstypes"; // https://github.com/jestjs/jest/issues/6028#issuecomment-567669082 function defuseRejection<T>(promise: Promise<T>) { promise.catch(() => {}); return promise; } function deepClone<T>(obj: T): T { return JSON.parse(JSON.stringify(obj)); } function reverseApsFrame(apsFrame: EmberApsFrame): EmberApsFrame { return Object.assign({}, apsFrame, {sourceEndpoint: apsFrame.destinationEndpoint, destinationEndpoint: apsFrame.sourceEndpoint}); } async function flushPromises(): Promise<void> { const {setImmediate} = await vi.importActual<typeof import("node:timers")>("node:timers"); return new Promise(setImmediate); } const TEMP_PATH = path.resolve("temp"); const STACK_CONFIG_PATH = path.join(TEMP_PATH, "stack_config.json"); const DEFAULT_NETWORK_OPTIONS: Readonly<NetworkOptions> = { panID: 24404, extendedPanID: [118, 185, 136, 236, 199, 244, 246, 85], channelList: [20], networkKey: [72, 97, 39, 230, 92, 72, 101, 148, 64, 225, 250, 214, 195, 31, 105, 71], networkKeyDistribute: false, }; const DEFAULT_SERIAL_PORT_OPTIONS: Readonly<SerialPortOptions> = { baudRate: 115200, rtscts: false, path: "MOCK", adapter: "ember", }; const DEFAULT_ADAPTER_OPTIONS: Readonly<AdapterOptions> = { concurrent: 16, disableLED: false, }; const DEFAULT_BACKUP: Readonly<UnifiedBackupStorage> = { metadata: { format: "zigpy/open-coordinator-backup", version: 1, source: "zigbee-herdsman@0.55.0", internal: { date: "2024-07-19T15:57:15.163Z", ezspVersion: 13, }, }, stack_specific: { ezsp: { hashed_tclk: "da85e5bac80c8a958b14d44f14c2ba16", }, }, coordinator_ieee: "1122334455667788", pan_id: "5f54", extended_pan_id: "76b988ecc7f4f655", nwk_update_id: 0, security_level: 5, channel: 20, channel_mask: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26], network_key: { key: "486127e65c48659440e1fad6c31f6947", sequence_number: 0, frame_counter: 16434, }, devices: [], }; const DEFAULT_COORDINATOR_IEEE: Eui64 = ZSpec.Utils.eui64LEBufferToHex(Buffer.from(DEFAULT_BACKUP.coordinator_ieee, "hex")); const DEFAULT_ADAPTER_NETWORK_PARAMETERS: EmberNetworkParameters = { extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, panId: DEFAULT_NETWORK_OPTIONS.panID, radioTxPower: 5, radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: 0, nwkManagerId: 0, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, }; let mockManufCode = Zcl.ManufacturerCode.SILICON_LABORATORIES; let mockAPSSequence = -1; // start at 0 let mockMessageTag = -1; // start at 0 let mockEzspEmitter = new EventEmitter<EmberEzspEventMap>(); const mockEzspRemoveAllListeners = vi.fn().mockImplementation((e) => { mockEzspEmitter.removeAllListeners(e); }); const mockEzspOn = vi.fn().mockImplementation((e, l) => { mockEzspEmitter.on(e, l); }); const mockEzspOnce = vi.fn().mockImplementation((e, l) => { mockEzspEmitter.once(e, l); }); const mockEzspStart = vi.fn().mockResolvedValue(EzspStatus.SUCCESS); const mockEzspStop = vi.fn(); const mockEzspSend = vi.fn().mockResolvedValue([SLStatus.OK, ++mockMessageTag]); const mockEzspSetMulticastTableEntry = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspSetManufacturerCode = vi.fn().mockImplementation((code) => { mockManufCode = code; }); const mockEzspReadAndClearCounters = vi.fn().mockResolvedValue([1, 2, 3, 4]); // not matching EmberCounterType, but doesn't matter here const mockEzspGetNetworkParameters = vi .fn() .mockResolvedValue([SLStatus.OK, EmberNodeType.COORDINATOR, deepClone(DEFAULT_ADAPTER_NETWORK_PARAMETERS)]); const mockEzspNetworkState = vi.fn().mockResolvedValue(EmberNetworkStatus.JOINED_NETWORK); const mockEzspGetEui64 = vi.fn().mockResolvedValue(DEFAULT_COORDINATOR_IEEE); const mockEzspSetConcentrator = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspSetSourceRouteDiscoveryMode = vi.fn().mockResolvedValue(1240 /* ms */); const mockEzspSetRadioIeee802154CcaMode = vi.fn().mockResolvedValue(SLStatus.OK); // not OK by default since used to detected unreged EP const mockEzspGetEndpointFlags = vi.fn().mockResolvedValue([SLStatus.NOT_FOUND, EzspEndpointFlag.DISABLED]); const mockEzspAddEndpoint = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspNetworkInit = vi.fn().mockImplementation((_networkInitStruct: EmberNetworkInitStruct) => { setTimeout(async () => { mockEzspEmitter.emit("stackStatus", SLStatus.NETWORK_UP); await flushPromises(); }, 300); return SLStatus.OK; }); const mockEzspExportKey = vi.fn().mockImplementation((context: SecManContext) => { switch (context.coreKeyType) { case SecManKeyType.NETWORK: { return [SLStatus.OK, {contents: Buffer.from(DEFAULT_BACKUP.network_key.key, "hex")} as SecManKey]; } case SecManKeyType.TC_LINK: { return [SLStatus.OK, {contents: Buffer.from(DEFAULT_BACKUP.stack_specific!.ezsp!.hashed_tclk!, "hex")} as SecManKey]; } } }); const mockEzspLeaveNetwork = vi.fn().mockImplementation(() => { setTimeout(async () => { mockEzspEmitter.emit("stackStatus", SLStatus.NETWORK_DOWN); await flushPromises(); }, 300); return SLStatus.OK; }); const mockEzspSetInitialSecurityState = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspSetExtendedSecurityBitmask = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspClearKeyTable = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspFormNetwork = vi.fn().mockImplementation((_parameters: EmberNetworkParameters) => { setTimeout(async () => { mockEzspEmitter.emit("stackStatus", SLStatus.NETWORK_UP); await flushPromises(); }, 300); return SLStatus.OK; }); const mockEzspStartWritingStackTokens = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspGetConfigurationValue = vi.fn().mockImplementation((config: EzspConfigId) => { switch (config) { case EzspConfigId.KEY_TABLE_SIZE: { return [SLStatus.OK, 0]; } } }); const mockEzspExportLinkKeyByIndex = vi.fn(); const mockEzspEraseKeyTableEntry = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspImportLinkKey = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspBroadcastNextNetworkKey = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspBroadcastNetworkKeySwitch = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspStartScan = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspVersion = vi.fn().mockImplementation((version: number) => [version, EZSP_STACK_TYPE_MESH, 0]); const mockEzspSetProtocolVersion = vi.fn(); const mockEzspGetVersionStruct = vi.fn().mockResolvedValue([ SLStatus.OK, { build: 135, major: 8, minor: 0, patch: 0, special: 0, type: EmberVersionType.GA, } as EmberVersion, ]); const mockEzspSetConfigurationValue = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspSetValue = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspSetPolicy = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspPermitJoining = vi.fn().mockImplementation((duration: number) => { setTimeout(async () => { mockEzspEmitter.emit("stackStatus", duration > 0 ? SLStatus.ZIGBEE_NETWORK_OPENED : SLStatus.ZIGBEE_NETWORK_CLOSED); await flushPromises(); }, 300); return SLStatus.OK; }); const mockEzspSendBroadcast = vi.fn().mockResolvedValue([SLStatus.OK, ++mockAPSSequence]); const mockEzspSendUnicast = vi.fn().mockResolvedValue([SLStatus.OK, ++mockAPSSequence]); const mockEzspGetNetworkKeyInfo = vi.fn().mockResolvedValue([ SLStatus.OK, { networkKeySet: true, alternateNetworkKeySet: false, networkKeySequenceNumber: DEFAULT_BACKUP.network_key.sequence_number, altNetworkKeySequenceNumber: 0, networkKeyFrameCounter: DEFAULT_BACKUP.network_key.frame_counter, } as SecManNetworkKeyInfo, ]); const mockEzspGetApsKeyInfo = vi.fn().mockResolvedValue([ SLStatus.OK, { bitmask: EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER, outgoingFrameCounter: 456, incomingFrameCounter: 0, ttlInSeconds: 0, } as SecManAPSKeyMetadata, ]); const mockEzspSetRadioPower = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspImportTransientKey = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspClearTransientLinkKeys = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspSetLogicalAndRadioChannel = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspSendRawMessage = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspSetNWKFrameCounter = vi.fn().mockResolvedValue(SLStatus.OK); const mockEzspSetAPSFrameCounter = vi.fn().mockResolvedValue(SLStatus.OK); vi.mock("../../../src/adapter/ember/uart/ash"); vi.mock("../../../src/adapter/ember/ezsp/ezsp", async (importOriginal) => ({ ...(await importOriginal()), Ezsp: vi.fn(() => ({ removeAllListeners: mockEzspRemoveAllListeners, on: mockEzspOn, once: mockEzspOnce, // only functions called from adapter ash: {readAndClearCounters: vi.fn().mockReturnValue([9, 8, 7])}, start: mockEzspStart, stop: mockEzspStop, send: mockEzspSend, ezspSetMulticastTableEntry: mockEzspSetMulticastTableEntry, ezspSetManufacturerCode: mockEzspSetManufacturerCode, ezspReadAndClearCounters: mockEzspReadAndClearCounters, ezspGetNetworkParameters: mockEzspGetNetworkParameters, ezspNetworkState: mockEzspNetworkState, ezspGetEui64: mockEzspGetEui64, ezspSetConcentrator: mockEzspSetConcentrator, ezspSetSourceRouteDiscoveryMode: mockEzspSetSourceRouteDiscoveryMode, ezspSetRadioIeee802154CcaMode: mockEzspSetRadioIeee802154CcaMode, ezspGetEndpointFlags: mockEzspGetEndpointFlags, ezspAddEndpoint: mockEzspAddEndpoint, ezspNetworkInit: mockEzspNetworkInit, ezspExportKey: mockEzspExportKey, ezspLeaveNetwork: mockEzspLeaveNetwork, ezspSetInitialSecurityState: mockEzspSetInitialSecurityState, ezspSetExtendedSecurityBitmask: mockEzspSetExtendedSecurityBitmask, ezspClearKeyTable: mockEzspClearKeyTable, ezspFormNetwork: mockEzspFormNetwork, ezspStartWritingStackTokens: mockEzspStartWritingStackTokens, ezspGetConfigurationValue: mockEzspGetConfigurationValue, ezspExportLinkKeyByIndex: mockEzspExportLinkKeyByIndex, ezspEraseKeyTableEntry: mockEzspEraseKeyTableEntry, ezspImportLinkKey: mockEzspImportLinkKey, ezspBroadcastNextNetworkKey: mockEzspBroadcastNextNetworkKey, ezspBroadcastNetworkKeySwitch: mockEzspBroadcastNetworkKeySwitch, ezspStartScan: mockEzspStartScan, ezspVersion: mockEzspVersion, setProtocolVersion: mockEzspSetProtocolVersion, ezspGetVersionStruct: mockEzspGetVersionStruct, ezspSetConfigurationValue: mockEzspSetConfigurationValue, ezspSetValue: mockEzspSetValue, ezspSetPolicy: mockEzspSetPolicy, ezspPermitJoining: mockEzspPermitJoining, ezspSendBroadcast: mockEzspSendBroadcast, ezspSendUnicast: mockEzspSendUnicast, ezspGetNetworkKeyInfo: mockEzspGetNetworkKeyInfo, ezspGetApsKeyInfo: mockEzspGetApsKeyInfo, ezspSetRadioPower: mockEzspSetRadioPower, ezspImportTransientKey: mockEzspImportTransientKey, ezspClearTransientLinkKeys: mockEzspClearTransientLinkKeys, ezspSetLogicalAndRadioChannel: mockEzspSetLogicalAndRadioChannel, ezspSendRawMessage: mockEzspSendRawMessage, ezspSetNWKFrameCounter: mockEzspSetNWKFrameCounter, ezspSetAPSFrameCounter: mockEzspSetAPSFrameCounter, })), })); const ezspMocks = [ mockEzspRemoveAllListeners, mockEzspOn, mockEzspOnce, mockEzspStart, mockEzspStop, mockEzspSend, mockEzspSetMulticastTableEntry, mockEzspSetManufacturerCode, mockEzspReadAndClearCounters, mockEzspGetNetworkParameters, mockEzspNetworkState, mockEzspGetEui64, mockEzspSetConcentrator, mockEzspSetSourceRouteDiscoveryMode, mockEzspSetRadioIeee802154CcaMode, mockEzspGetEndpointFlags, mockEzspAddEndpoint, mockEzspNetworkInit, mockEzspExportKey, mockEzspLeaveNetwork, mockEzspSetInitialSecurityState, mockEzspSetExtendedSecurityBitmask, mockEzspClearKeyTable, mockEzspFormNetwork, mockEzspStartWritingStackTokens, mockEzspGetConfigurationValue, mockEzspExportLinkKeyByIndex, mockEzspEraseKeyTableEntry, mockEzspImportLinkKey, mockEzspBroadcastNextNetworkKey, mockEzspBroadcastNetworkKeySwitch, mockEzspStartScan, mockEzspVersion, mockEzspSetProtocolVersion, mockEzspGetVersionStruct, mockEzspSetConfigurationValue, mockEzspSetValue, mockEzspSetPolicy, mockEzspPermitJoining, mockEzspSendBroadcast, mockEzspSendUnicast, mockEzspGetNetworkKeyInfo, mockEzspGetApsKeyInfo, mockEzspSetRadioPower, mockEzspImportTransientKey, mockEzspClearTransientLinkKeys, mockEzspSetLogicalAndRadioChannel, mockEzspSendRawMessage, mockEzspSetNWKFrameCounter, mockEzspSetAPSFrameCounter, ]; describe("Ember Adapter Layer", () => { let adapter: EmberAdapter; let backupPath: string; const loggerSpies = { debug: vi.spyOn(logger, "debug"), info: vi.spyOn(logger, "info"), warning: vi.spyOn(logger, "warning"), error: vi.spyOn(logger, "error"), }; const deleteCoordinatorBackup = () => { if (existsSync(backupPath)) { unlinkSync(backupPath); } }; const deleteStackConfig = () => { if (existsSync(STACK_CONFIG_PATH)) { unlinkSync(STACK_CONFIG_PATH); } }; const takeResetCodePath = () => { deleteCoordinatorBackup(); mockEzspGetNetworkParameters.mockResolvedValueOnce([ SLStatus.OK, EmberNodeType.COORDINATOR, { extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, panId: 1234, radioTxPower: 5, radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: 0, nwkManagerId: 0, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, } as EmberNetworkParameters, ]); }; const takeRestoredCodePath = () => { mockEzspGetNetworkParameters.mockResolvedValueOnce([ SLStatus.OK, EmberNodeType.COORDINATOR, { extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, panId: 1234, radioTxPower: 5, radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: 0, nwkManagerId: 0, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, } as EmberNetworkParameters, ]); }; const clearMocks = () => { for (const mock of ezspMocks) { mock.mockClear(); } loggerSpies.debug.mockClear(); loggerSpies.info.mockClear(); loggerSpies.warning.mockClear(); loggerSpies.error.mockClear(); }; beforeAll(() => { if (!existsSync(TEMP_PATH)) { mkdirSync(TEMP_PATH); } else { // just in case, remove previous remnants deleteCoordinatorBackup(); deleteStackConfig(); } }); afterAll(() => { deleteCoordinatorBackup(); deleteStackConfig(); }); beforeEach(() => { vi.useFakeTimers(); backupPath = path.join(TEMP_PATH, "ember_coordinator_backup.json"); writeFileSync(backupPath, JSON.stringify(DEFAULT_BACKUP, undefined, 2)); mockManufCode = Zcl.ManufacturerCode.SILICON_LABORATORIES; mockAPSSequence = -1; mockMessageTag = -1; // make sure emitter is reset too mockEzspEmitter = new EventEmitter(); clearMocks(); }); afterEach(() => { vi.useRealTimers(); }); it("Creates default instance", () => { adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); expect(adapter).toBeInstanceOf(EmberAdapter); expect(adapter.stackConfig).toStrictEqual(DEFAULT_STACK_CONFIG); }); it("Loads custom stack config", () => { const config = { CONCENTRATOR_RAM_TYPE: "low", CONCENTRATOR_MIN_TIME: 1, CONCENTRATOR_MAX_TIME: 31, CONCENTRATOR_ROUTE_ERROR_THRESHOLD: 5, CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD: 2, CONCENTRATOR_MAX_HOPS: 5, MAX_END_DEVICE_CHILDREN: 16, TRANSIENT_DEVICE_TIMEOUT: 1000, END_DEVICE_POLL_TIMEOUT: 12, TRANSIENT_KEY_TIMEOUT_S: 500, CCA_MODE: "SIGNAL_AND_RSSI", }; writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); expect(adapter.stackConfig).toStrictEqual(config); // cleanup unlinkSync(STACK_CONFIG_PATH); }); it("Loads only valid custom stack config", () => { const config = { CONCENTRATOR_RAM_TYPE: "bad", CONCENTRATOR_MIN_TIME: -1, CONCENTRATOR_MAX_TIME: 15, CONCENTRATOR_ROUTE_ERROR_THRESHOLD: 500, CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD: 200, CONCENTRATOR_MAX_HOPS: 35, MAX_END_DEVICE_CHILDREN: 65, TRANSIENT_DEVICE_TIMEOUT: 65536, END_DEVICE_POLL_TIMEOUT: 15, TRANSIENT_KEY_TIMEOUT_S: 65536, CCA_MODE: "abcd", }; writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); expect(adapter.stackConfig).toStrictEqual(DEFAULT_STACK_CONFIG); // cleanup unlinkSync(STACK_CONFIG_PATH); }); it("Loads only valid custom stack config - null CCA_MODE", () => { const config = { CCA_MODE: null, }; writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); expect(adapter.stackConfig).toStrictEqual(DEFAULT_STACK_CONFIG); // cleanup unlinkSync(STACK_CONFIG_PATH); }); it("Uses default concurrency for queue if not supplied/valid", () => { adapter = new EmberAdapter( DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, Object.assign({}, DEFAULT_ADAPTER_OPTIONS, {concurrent: undefined}), ); // @ts-expect-error private expect(adapter.queue.concurrent).toStrictEqual(16); }); it("Starts with resumed when everything matches", async () => { adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("resumed"); expect(mockEzspSetProtocolVersion).toHaveBeenCalledWith(EZSP_PROTOCOL_VERSION); expect( // @ts-expect-error private adapter.networkCache, ).toStrictEqual({ eui64: DEFAULT_COORDINATOR_IEEE, parameters: { extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, panId: DEFAULT_NETWORK_OPTIONS.panID, radioTxPower: 5, radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: 0, nwkManagerId: 0, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, } as EmberNetworkParameters, } as NetworkCache); }); it("Starts with custom stack config", async () => { const config = { CONCENTRATOR_RAM_TYPE: "low", CONCENTRATOR_MIN_TIME: 1, CONCENTRATOR_MAX_TIME: 31, CONCENTRATOR_ROUTE_ERROR_THRESHOLD: 5, CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD: 2, CONCENTRATOR_MAX_HOPS: 5, MAX_END_DEVICE_CHILDREN: 16, TRANSIENT_DEVICE_TIMEOUT: 1000, END_DEVICE_POLL_TIMEOUT: 12, TRANSIENT_KEY_TIMEOUT_S: 500, CCA_MODE: "SIGNAL_AND_RSSI", }; writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("resumed"); expect(mockEzspSetValue).toHaveBeenCalledWith(EzspValueId.TRANSIENT_DEVICE_TIMEOUT, 2, lowHighBytes(config.TRANSIENT_DEVICE_TIMEOUT)); expect(mockEzspSetConfigurationValue).toHaveBeenCalledWith(EzspConfigId.MAX_END_DEVICE_CHILDREN, config.MAX_END_DEVICE_CHILDREN); expect(mockEzspSetConfigurationValue).toHaveBeenCalledWith(EzspConfigId.END_DEVICE_POLL_TIMEOUT, config.END_DEVICE_POLL_TIMEOUT); expect(mockEzspSetConfigurationValue).toHaveBeenCalledWith(EzspConfigId.TRANSIENT_KEY_TIMEOUT_S, config.TRANSIENT_KEY_TIMEOUT_S); expect(mockEzspSetConcentrator).toHaveBeenCalledWith( true, EMBER_LOW_RAM_CONCENTRATOR, config.CONCENTRATOR_MIN_TIME, config.CONCENTRATOR_MAX_TIME, config.CONCENTRATOR_ROUTE_ERROR_THRESHOLD, config.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, config.CONCENTRATOR_MAX_HOPS, ); expect(mockEzspSetRadioIeee802154CcaMode).toHaveBeenCalledWith(IEEE802154CcaMode.SIGNAL_AND_RSSI); // cleanup unlinkSync(STACK_CONFIG_PATH); }); it("Starts with custom stack config invalid CCA_MODE", async () => { const config = { CCA_MODE: "abcd", }; writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("resumed"); expect(mockEzspSetRadioIeee802154CcaMode).toHaveBeenCalledTimes(0); // cleanup unlinkSync(STACK_CONFIG_PATH); }); it("Starts with restored when no network in adapter", async () => { adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const expectedNetParams: EmberNetworkParameters = { extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, panId: DEFAULT_NETWORK_OPTIONS.panID, radioTxPower: 5, radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: 0, nwkManagerId: 0, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, }; mockEzspNetworkInit.mockResolvedValueOnce(SLStatus.NOT_JOINED); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); expect(mockEzspSetNWKFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.network_key.frame_counter); // expect(mockEzspSetAPSFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.???.???); expect(mockEzspFormNetwork).toHaveBeenCalledWith(expectedNetParams); await expect(result).resolves.toStrictEqual("restored"); }); it("Starts with restored when network param mismatch but backup available", async () => { adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const expectedNetParams: EmberNetworkParameters = { extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, panId: DEFAULT_NETWORK_OPTIONS.panID, radioTxPower: 5, radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: 0, nwkManagerId: 0, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, }; mockEzspGetNetworkParameters.mockResolvedValueOnce([ SLStatus.OK, EmberNodeType.COORDINATOR, { extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, panId: 1234, radioTxPower: 5, radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: 0, nwkManagerId: 0, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, } as EmberNetworkParameters, ]); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); expect(mockEzspSetNWKFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.network_key.frame_counter); // expect(mockEzspSetAPSFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.???.???); expect(mockEzspFormNetwork).toHaveBeenCalledWith(expectedNetParams); await expect(result).resolves.toStrictEqual("restored"); }); it("Starts with restored when network key mismatch but backup available", async () => { adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const expectedNetParams: EmberNetworkParameters = { extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, panId: DEFAULT_NETWORK_OPTIONS.panID, radioTxPower: 5, radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: 0, nwkManagerId: 0, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, }; mockEzspGetNetworkParameters.mockResolvedValueOnce([SLStatus.OK, EmberNodeType.COORDINATOR, expectedNetParams]); const contents = Buffer.from(DEFAULT_BACKUP.network_key.key, "hex").fill(0xff); mockEzspExportKey.mockResolvedValueOnce([SLStatus.OK, {contents} as SecManKey]); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("restored"); expect(mockEzspSetNWKFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.network_key.frame_counter); // expect(mockEzspSetAPSFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.???.???); expect(mockEzspFormNetwork).toHaveBeenCalledWith(expectedNetParams); }); it("Starts with reset when networks mismatch but no backup available", async () => { adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); deleteCoordinatorBackup(); mockEzspGetNetworkParameters.mockResolvedValueOnce([ SLStatus.OK, EmberNodeType.COORDINATOR, { extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, panId: 1234, radioTxPower: 5, radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: 0, nwkManagerId: 0, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, } as EmberNetworkParameters, ]); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("reset"); }); it("Starts with reset when backup/config mismatch", async () => { adapter = new EmberAdapter( Object.assign({}, DEFAULT_NETWORK_OPTIONS, {panID: 1234}), DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS, ); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("reset"); expect(mockEzspSetNWKFrameCounter).toHaveBeenCalledTimes(0); // expect(mockEzspSetAPSFrameCounter).toHaveBeenCalledTimes(0); expect(mockEzspFormNetwork).toHaveBeenCalledWith({ panId: 1234, extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, radioTxPower: 5, // default when setting `transmitPower` is null/zero radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: EmberJoinMethod.MAC_ASSOCIATION, nwkManagerId: ZSpec.COORDINATOR_ADDRESS, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, } as EmberNetworkParameters); }); it("Starts with reset and forms with given transmit power", async () => { adapter = new EmberAdapter( Object.assign({}, DEFAULT_NETWORK_OPTIONS, {panID: 1234}), DEFAULT_SERIAL_PORT_OPTIONS, backupPath, Object.assign({}, DEFAULT_ADAPTER_OPTIONS, {transmitPower: 10}), ); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("reset"); expect(mockEzspSetNWKFrameCounter).toHaveBeenCalledTimes(0); // expect(mockEzspSetAPSFrameCounter).toHaveBeenCalledTimes(0); expect(mockEzspFormNetwork).toHaveBeenCalledWith({ panId: 1234, extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, radioTxPower: 10, radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: EmberJoinMethod.MAC_ASSOCIATION, nwkManagerId: ZSpec.COORDINATOR_ADDRESS, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, } as EmberNetworkParameters); }); it("Starts with mismatching transmit power", async () => { adapter = new EmberAdapter( DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, Object.assign({}, DEFAULT_ADAPTER_OPTIONS, {transmitPower: 10}), ); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("resumed"); expect(mockEzspSetRadioPower).toHaveBeenCalledTimes(1); expect(mockEzspSetRadioPower).toHaveBeenCalledWith(10); }); it("Starts with matching transmit power after form", async () => { adapter = new EmberAdapter( DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, Object.assign({}, DEFAULT_ADAPTER_OPTIONS, {transmitPower: 10}), ); mockEzspNetworkInit.mockResolvedValueOnce(SLStatus.NOT_JOINED); mockEzspGetNetworkParameters.mockResolvedValueOnce([ SLStatus.OK, EmberNodeType.COORDINATOR, { extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!, panId: DEFAULT_NETWORK_OPTIONS.panID, radioTxPower: 10, radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0], joinMethod: 0, nwkManagerId: 0, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, } as EmberNetworkParameters, ]); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("restored"); expect(mockEzspSetRadioPower).toHaveBeenCalledTimes(0); }); it("Starts with mismatching transmit power, failure does not present start", async () => { adapter = new EmberAdapter( DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, Object.assign({}, DEFAULT_ADAPTER_OPTIONS, {transmitPower: 12}), ); mockEzspSetRadioPower.mockResolvedValueOnce(SLStatus.FAIL); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("resumed"); expect(mockEzspSetRadioPower).toHaveBeenCalledTimes(1); expect(mockEzspSetRadioPower).toHaveBeenCalledWith(12); expect(loggerSpies.error).toHaveBeenCalledWith("Failed to set transmit power to 12 status=FAIL.", "zh:ember"); }); it("Fails to start when EZSP layer fails to start", async () => { adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); mockEzspStart.mockResolvedValueOnce(EzspStatus.HOST_FATAL_ERROR); const result = adapter.start(); await expect(result).rejects.toThrow(`Failed to start EZSP layer with status=${EzspStatus[EzspStatus.HOST_FATAL_ERROR]}.`); }); it.each([ [ "if NCP has improper stack type", () => { mockEzspVersion.mockResolvedValueOnce([14, 1, 123]); }, "Stack type 1 is not expected!", ], [ "if NCP version unsupported", () => { mockEzspVersion.mockResolvedValueOnce([12, EZSP_STACK_TYPE_MESH, 123]); }, `Adapter EZSP protocol version (12) is not supported by Host [${EZSP_MIN_PROTOCOL_VERSION}-${EZSP_PROTOCOL_VERSION}].`, ], [ "if NCP has old style version number", () => { mockEzspGetVersionStruct.mockResolvedValueOnce([SLStatus.INVALID_PARAMETER, 0]); }, "NCP has old-style version number. Not supported.", ], [ "if network is not valid by end of init sequence", () => { mockEzspGetNetworkParameters .mockResolvedValueOnce([SLStatus.OK, EmberNodeType.COORDINATOR, deepClone(DEFAULT_ADAPTER_NETWORK_PARAMETERS)]) .mockResolvedValueOnce([SLStatus.FAIL, 0, {}]); }, "Failed to get network parameters with status=FAIL.", ], [ "if could not set concentrator", () => { mockEzspSetConcentrator.mockResolvedValueOnce(SLStatus.FAIL); }, "[CONCENTRATOR] Failed to set concentrator with status=FAIL.", ], [ "if could not add endpoint", () => { mockEzspAddEndpoint.mockResolvedValueOnce(SLStatus.FAIL); }, `Failed to register endpoint '1' with status=FAIL.`, ], [ "if could not set multicast table entry", () => { mockEzspSetMulticastTableEntry.mockResolvedValueOnce(SLStatus.FAIL); }, `Failed to register group '0' in multicast table with status=FAIL.`, ], [ "if could not set TC key request policy", () => { mockEzspSetPolicy .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.BINDING_MODIFICATION_POLICY .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY .mockResolvedValueOnce(SLStatus.FAIL); // EzspPolicyId.TC_KEY_REQUEST_POLICY }, "[INIT TC] Failed to set EzspPolicyId TC_KEY_REQUEST_POLICY to ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY with status=FAIL.", ], [ "if could not set app key request policy", () => { mockEzspSetPolicy .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.BINDING_MODIFICATION_POLICY .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.TC_KEY_REQUEST_POLICY .mockResolvedValueOnce(SLStatus.FAIL); // EzspPolicyId.APP_KEY_REQUEST_POLICY }, "[INIT TC] Failed to set EzspPolicyId APP_KEY_REQUEST_POLICY to DENY_APP_KEY_REQUESTS with status=FAIL.", ], [ "if could not set app key request policy", () => { mockEzspSetPolicy .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.BINDING_MODIFICATION_POLICY .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.TC_KEY_REQUEST_POLICY .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.APP_KEY_REQUEST_POLICY .mockResolvedValueOnce(SLStatus.FAIL); // EzspPolicyId.TRUST_CENTER_POLICY }, "[INIT TC] Failed to set join policy to USE_PRECONFIGURED_KEY with status=FAIL.", ], [ "if could not init network", () => { mockEzspNetworkInit.mockResolvedValueOnce(SLStatus.FAIL); }, "[INIT TC] Failed network init request with status=FAIL.", ], [ "if could not export network key", () => { mockEzspExportKey.mockResolvedValueOnce([SLStatus.FAIL, Buffer.alloc(16)]); }, "[INIT TC] Failed to export Network Key with status=FAIL.", ], [ "if could not leave network", () => { // force leave code path mockEzspGetNetworkParameters.mockResolvedValueOnce([SLStatus.FAIL, 0, {}]); mockEzspLeaveNetwork.mockResolvedValueOnce(SLStatus.FAIL); }, "[INIT TC] Failed leave network request with status=FAIL.", ], [ "if form could not set NWK frame counter", () => { takeRestoredCodePath(); mockEzspSetNWKFrameCounter.mockResolvedValueOnce(SLStatus.FAIL); }, "[INIT FORM] Failed to set NWK frame counter with status=FAIL.", ], // [ // 'if form could not set TC APS frame counter', // () => { // takeRestoredCodePath(); // mockEzspSetAPSFrameCounter.mockResolvedValueOnce(SLStatus.FAIL); // }, // `[INIT FORM] Failed to set TC APS frame counter with status=FAIL.`, // ], [ "if form could not set initial security state", () => { takeResetCodePath(); mockEzspSetInitialSecurityState.mockResolvedValueOnce(SLStatus.FAIL); }, "[INIT FORM] Failed to set initial security state with status=FAIL.", ], [ "if form could not set extended security bitmask", () => { takeResetCodePath(); mockEzspSetExtendedSecurityBitmask.mockResolvedValueOnce(SLStatus.FAIL); }, "[INIT FORM] Failed to set extended security bitmask to 272 with status=FAIL.", ], [ "if could not form network", () => { takeResetCodePath(); mockEzspFormNetwork.mockResolvedValueOnce(SLStatus.FAIL); }, "[INIT FORM] Failed form network request with status=FAIL.", ], [ "if backup corrupted", () => { writeFileSync(backupPath, "abcd"); }, "[BACKUP] Coordinator backup is corrupted.", ], [ "if backup unsupported", () => { const customBackup = deepClone(DEFAULT_BACKUP); // @ts-expect-error mock override customBackup.metadata.version = 2; writeFileSync(backupPath, JSON.stringify(customBackup, undefined, 2)); }, "[BACKUP] Unsupported open coordinator backup version (version=2).", ], [ "if backup not EmberZNet stack specific", () => { const customBackup = deepClone(DEFAULT_BACKUP); customBackup.stack_specific!.ezsp = undefined; writeFileSync(backupPath, JSON.stringify(customBackup, undefined, 2)); }, "[BACKUP] Current backup file is not for EmberZNet stack.", ], [ "if backup not EmberZNet EZSP version", () => { const customBackup = deepClone(DEFAULT_BACKUP); customBackup.metadata.internal.ezspVersion = undefined; writeFileSync(backupPath, JSON.stringify(customBackup, undefined, 2)); }, "[BACKUP] Current backup file is not for EmberZNet stack.", ], [ "if backup unknown format", () => { const customBackup = deepClone(DEFAULT_BACKUP); // @ts-expect-error mock override customBackup.metadata.format = "unknown"; writeFileSync(backupPath, JSON.stringify(customBackup, undefined, 2)); }, "[BACKUP] Unknown backup format.", ], ])("Fails to start %s", async (_reason, setup, error) => { adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); setup(); const result = defuseRejection(adapter.start()); await vi.advanceTimersByTimeAsync(5000); await expect(result).rejects.toThrow(error); }); it("Warns if NCP has non-GA firmware", async () => { const type: EmberVersionType = EmberVersionType.ALPHA_1; mockEzspGetVersionStruct.mockResolvedValueOnce([ SLStatus.OK, { build: 135, major: 8, minor: 0, patch: 0, special: 0, type, } as EmberVersion, ]); adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("resumed"); expect(loggerSpies.warning).toHaveBeenCalledWith(`Adapter is running a non-GA version (${EmberVersionType[type]}).`, "zh:ember"); }); it("Switches EZSP protocol when supported", async () => { mockEzspVersion.mockResolvedValueOnce([EZSP_MIN_PROTOCOL_VERSION, EZSP_STACK_TYPE_MESH, 123]); adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("resumed"); expect(mockEzspVersion).toHaveBeenNthCalledWith(1, EZSP_PROTOCOL_VERSION); expect(mockEzspVersion).toHaveBeenNthCalledWith(2, EZSP_MIN_PROTOCOL_VERSION); expect(mockEzspSetProtocolVersion).toHaveBeenCalledWith(EZSP_MIN_PROTOCOL_VERSION); }); it("Logs failed set config value on start", async () => { mockEzspSetConfigurationValue.mockResolvedValueOnce(SLStatus.ALLOCATION_FAILED); adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("resumed"); expect(loggerSpies.info).toHaveBeenCalledWith( `[EzspConfigId] Failed to SET '${EzspConfigId[EzspConfigId.TRUST_CENTER_ADDRESS_CACHE_SIZE]}' TO '2' with status=${SLStatus[SLStatus.ALLOCATION_FAILED]}. Firmware value will be used instead.`, "zh:ember", ); }); it("Starts and skips adding endpoint if already present", async () => { adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); mockEzspGetEndpointFlags .mockResolvedValueOnce([SLStatus.NOT_FOUND, EzspEndpointFlag.DISABLED]) .mockResolvedValueOnce([SLStatus.OK, EzspEndpointFlag.ENABLED]); // mock GP already registered const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("resumed"); expect(mockEzspAddEndpoint).toHaveBeenCalledTimes(1); const ep = FIXED_ENDPOINTS[0]; expect(mockEzspAddEndpoint).toHaveBeenCalledWith( ep.endpoint, ep.profileId, ep.deviceId, ep.deviceVersion, ep.inClusterList.slice(), // copy ep.outClusterList.slice(), // copy ); }); it("Starts and detects when network key frame counter will soon wrap to 0", async () => { const customBackup = deepClone(DEFAULT_BACKUP); customBackup.network_key.frame_counter = 0xfeeeeeef; writeFileSync(backupPath, JSON.stringify(customBackup, undefined, 2)); adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("resumed"); expect(logger.warning).toHaveBeenCalledWith( "[INIT TC] Network key frame counter is reaching its limit. A new network key will have to be instaured soon.", "zh:ember", ); }); it("Starts and soft-fails if unable to clear key table", async () => { takeResetCodePath(); mockEzspClearKeyTable.mockResolvedValueOnce(SLStatus.FAIL); adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const result = adapter.start(); await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("reset"); expect(loggerSpies.error).toHaveBeenCalledWith("[INIT FORM] Failed to clear key table with status=FAIL.", "zh:ember"); }); it("Starts but ignores backup if unsupported version", async () => { const customBackup = deepClone(DEFAULT_BACKUP); customBackup.metadata.internal.ezspVersion = 11; writeFileSync(backupPath, JSON.stringify(customBackup, undefined, 2)); adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const result = adapter.start(); const old = `${backupPath}.old`; await vi.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual("resumed"); expect(existsSync(old)).toBeTruthy(); expect(loggerSpies.warning).toHaveBeenCalledWith( "[BACKUP] Current backup f