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