zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
3,450 lines • 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 file is from an unsupported EZSP version. Renaming and ignoring.",
"zh:ember",
);
// cleanup
unlinkSync(old);
});
describe("When started", () => {
beforeEach(async () => {
adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS);
const result = adapter.start();
await vi.advanceTimersByTimeAsync(5000);
await result;
// clean slate "post-start"
clearMocks();
});
it("Stops Ezsp layer on stop", async () => {
// @ts-expect-error private
const ezspStopSpy = vi.spyOn(adapter.ezsp, "stop");
// @ts-expect-error private
const ezspRemoveAllListenersSpy = vi.spyOn(adapter.ezsp, "removeAllListeners");
await adapter.stop();
expect(ezspStopSpy).toHaveBeenCalledTimes(1);
expect(ezspRemoveAllListenersSpy).toHaveBeenCalledTimes(1);
});
it("Retrieves parameters from cache when cache valid", async () => {
await expect(adapter.emberGetEui64()).resolves.toStrictEqual(DEFAULT_COORDINATOR_IEEE);
expect(mockEzspGetEui64).toHaveBeenCalledTimes(0);
await expect(adapter.emberGetPanId()).resolves.toStrictEqual(DEFAULT_NETWORK_OPTIONS.panID);
expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(0);
await expect(adapter.emberGetExtendedPanId()).resolves.toStrictEqual(DEFAULT_NETWORK_OPTIONS.extendedPanID!);
expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(0);
await expect(adapter.emberGetRadioChannel()).resolves.toStrictEqual(DEFAULT_NETWORK_OPTIONS.channelList[0]);
expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(0);
});
it("Retrieves parameters from NCP when cache invalid", async () => {
adapter.clearNetworkCache();
await expect(adapter.emberGetEui64()).resolves.toStrictEqual(DEFAULT_COORDINATOR_IEEE);
expect(mockEzspGetEui64).toHaveBeenCalledTimes(1);
adapter.clearNetworkCache();
await expect(adapter.emberGetPanId()).resolves.toStrictEqual(DEFAULT_NETWORK_OPTIONS.panID);
expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(1);
adapter.clearNetworkCache();
await expect(adapter.emberGetExtendedPanId()).resolves.toStrictEqual(DEFAULT_NETWORK_OPTIONS.extendedPanID!);
expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(2);
adapter.clearNetworkCache();
await expect(adapter.emberGetRadioChannel()).resolves.toStrictEqual(DEFAULT_NETWORK_OPTIONS.channelList[0]);
expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(3);
});
it("Throws when failed to retrieve parameter from NCP", async () => {
mockEzspGetNetworkParameters
.mockResolvedValueOnce([SLStatus.FAIL, 0, {}])
.mockResolvedValueOnce([SLStatus.FAIL, 0, {}])
.mockResolvedValueOnce([SLStatus.FAIL, 0, {}]);
adapter.clearNetworkCache();
const p1 = defuseRejection(adapter.emberGetPanId());
await vi.advanceTimersByTimeAsync(5000);
await expect(p1).rejects.toThrow("Failed to get PAN ID (via network parameters) with status=FAIL.");
adapter.clearNetworkCache();
const p2 = defuseRejection(adapter.emberGetExtendedPanId());
await vi.advanceTimersByTimeAsync(5000);
await expect(p2).rejects.toThrow("Failed to get Extended PAN ID (via network parameters) with status=FAIL.");
adapter.clearNetworkCache();
const p3 = defuseRejection(adapter.emberGetRadioChannel());
await vi.advanceTimersByTimeAsync(5000);
await expect(p3).rejects.toThrow("Failed to get radio channel (via network parameters) with status=FAIL.");
});
it("Logs stack status change", async () => {
mockEzspEmitter.emit("stackStatus", SLStatus.ZIGBEE_TRUST_CENTER_SWAP_EUI_HAS_CHANGED);
await flushPromises();
expect(loggerSpies.debug).toHaveBeenCalledWith(
`[STACK STATUS] ${SLStatus[SLStatus.ZIGBEE_TRUST_CENTER_SWAP_EUI_HAS_CHANGED]}.`,
"zh:ember",
);
});
it("Handles message delivery failure", async () => {
let apsFrame: EmberApsFrame = {
profileId: ZSpec.HA_PROFILE_ID,
clusterId: Zcl.Clusters.genBasic.ID,
sourceEndpoint: 1,
destinationEndpoint: 1,
options: 0,
groupId: 0,
sequence: 0,
};
mockEzspEmitter.emit("messageSent", SLStatus.ZIGBEE_DELIVERY_FAILED, EmberOutgoingMessageType.BROADCAST, 1234, apsFrame, 1);
await flushPromises();
expect(loggerSpies.error).toHaveBeenCalledWith(`Delivery of BROADCAST failed for '1234'.`, "zh:ember");
const spyDeliveryFailedFor = vi.spyOn(
// @ts-expect-error private
adapter.oneWaitress,
"deliveryFailedFor",
);
apsFrame = {
profileId: ZSpec.HA_PROFILE_ID,
clusterId: Zcl.Clusters.genBasic.ID,
sourceEndpoint: 1,
destinationEndpoint: 1,
options: 0,
groupId: 0,
sequence: 0,
};
mockEzspEmitter.emit("messageSent", SLStatus.ZIGBEE_DELIVERY_FAILED, EmberOutgoingMessageType.DIRECT, 1234, apsFrame, 1);
await flushPromises();
expect(spyDeliveryFailedFor).toHaveBeenCalledTimes(1);
expect(spyDeliveryFailedFor).toHaveBeenCalledWith(1234, apsFrame);
});
it("Registers message unknown group in multicast table", async () => {
// @ts-expect-error private
const tableIdx = adapter.multicastTable.length;
const apsFrame = {
profileId: ZSpec.HA_PROFILE_ID,
clusterId: Zcl.Clusters.genBasic.ID,
sourceEndpoint: 1,
destinationEndpoint: 0xff,
options: 0,
groupId: 123,
sequence: 0,
};
mockEzspEmitter.emit("messageSent", SLStatus.OK, EmberOutgoingMessageType.MULTICAST, 1234, apsFrame, 1);
await flushPromises();
expect(mockEzspSetMulticastTableEntry).toHaveBeenCalledTimes(1);
expect(mockEzspSetMulticastTableEntry).toHaveBeenCalledWith(tableIdx, {
multicastId: 123,
endpoint: FIXED_ENDPOINTS[0].endpoint,
networkIndex: FIXED_ENDPOINTS[0].networkIndex,
} as EmberMulticastTableEntry);
expect(
// @ts-expect-error private
adapter.multicastTable.length,
).toStrictEqual(tableIdx + 1);
});
it("Fails to register message unknown group in multicast table", async () => {
mockEzspSetMulticastTableEntry.mockResolvedValueOnce(SLStatus.FAIL);
// @ts-expect-error private
const tableIdx = adapter.multicastTable.length;
const apsFrame = {
profileId: ZSpec.HA_PROFILE_ID,
clusterId: Zcl.Clusters.genBasic.ID,
sourceEndpoint: 1,
destinationEndpoint: 0xff,
options: 0,
groupId: 123,
sequence: 0,
};
mockEzspEmitter.emit("messageSent", SLStatus.OK, EmberOutgoingMessageType.MULTICAST, 1234, apsFrame, 1);
await flushPromises();
expect(mockEzspSetMulticastTableEntry).toHaveBeenCalledTimes(1);
expect(mockEzspSetMulticastTableEntry).toHaveBeenCalledWith(tableIdx, {
multicastId: 123,
endpoint: FIXED_ENDPOINTS[0].endpoint,
networkIndex: FIXED_ENDPOINTS[0].networkIndex,
} as EmberMulticastTableEntry);
expect(
// @ts-expect-error private
adapter.multicastTable.length,
).toStrictEqual(tableIdx); // not increased, entry was removed
});
it("Emits network address event on ZDO NETWORK_ADDRESS_RESPONSE", async () => {
const spyResolveZDO = vi.spyOn(
// @ts-expect-error private
adapter.oneWaitress,
"resolveZDO",
);
const spyEmit = vi.spyOn(adapter, "emit");
const sender = 1234;
const apsFrame: EmberApsFrame = {
profileId: Zdo.ZDO_PROFILE_ID,
clusterId: Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE,
sourceEndpoint: Zdo.ZDO_ENDPOINT,
destinationEndpoint: Zdo.ZDO_ENDPOINT,
options: 0,
groupId: 0,
sequence: 0,
};
mockEzspEmitter.emit(
"zdoResponse",
apsFrame,
sender,
Buffer.from([1, Zdo.Status.SUCCESS, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x11, 0x22, 0x33, 0xd2, 0x04]),
);
await flushPromises();
const zdoResponse = [
Zdo.Status.SUCCESS,
{
eui64: "0x332211eeddccbbaa",
nwkAddress: sender,
startIndex: 0,
assocDevList: [],
} as ZdoTypes.NetworkAddressResponse,
];
expect(spyResolveZDO).toHaveBeenCalledTimes(1);
expect(spyResolveZDO).toHaveBeenCalledWith("0x332211eeddccbbaa", apsFrame, zdoResponse);
expect(spyEmit).toHaveBeenCalledWith("zdoResponse", Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, zdoResponse);
});
it("Emits device announce event on ZDO END_DEVICE_ANNOUNCE", async () => {
const spyResolveZDO = vi.spyOn(
// @ts-expect-error private
adapter.oneWaitress,
"resolveZDO",
);
const spyEmit = vi.spyOn(adapter, "emit");
const sender = 1234;
const apsFrame: EmberApsFrame = {
profileId: Zdo.ZDO_PROFILE_ID,
clusterId: Zdo.ClusterId.END_DEVICE_ANNOUNCE,
sourceEndpoint: Zdo.ZDO_ENDPOINT,
destinationEndpoint: Zdo.ZDO_ENDPOINT,
options: 0,
groupId: 0,
sequence: 0,
};
mockEzspEmitter.emit("zdoResponse", apsFrame, sender, Buffer.from([1, 0xd2, 0x04, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x11, 0x22, 0x33, 6]));
await flushPromises();
const zdoResponse = [
Zdo.Status.SUCCESS,
{
nwkAddress: sender,
eui64: "0x332211eeddccbbaa",
capabilities: {
alternatePANCoordinator: 0,
deviceType: 1,
powerSource: 1,
rxOnWhenIdle: 0,
reserved1: 0,
reserved2: 0,
securityCapability: 0,
allocateAddress: 0,
},
} as ZdoTypes.EndDeviceAnnounce,
];
expect(spyResolveZDO).toHaveBeenCalledTimes(1);
expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, zdoResponse);
expect(spyEmit).toHaveBeenCalledWith("zdoResponse", Zdo.ClusterId.END_DEVICE_ANNOUNCE, zdoResponse);
});
it("Emits ZCL payload on incoming message", async () => {
const spyResolveZCL = vi.spyOn(
// @ts-expect-error private
adapter.oneWaitress,
"resolveZCL",
);
const spyEmit = vi.spyOn(adapter, "emit");
const sender = 1234;
const apsFrame: EmberApsFrame = {
profileId: ZSpec.HA_PROFILE_ID,
clusterId: Zcl.Clusters.genBasic.ID,
sourceEndpoint: 2,
destinationEndpoint: 1,
options: 0,
groupId: 0,
sequence: 0,
};
const lastHopLqi = 252;
// Received Zigbee message from '0x', type 'readResponse', cluster 'genBasic', data '{"zclVersion":3}' from endpoint 1 with groupID 0
const messageContents = Buffer.from("1803010000002003", "hex");
mockEzspEmitter.emit("incomingMessage", EmberIncomingMessageType.UNICAST, apsFrame, lastHopLqi, sender, messageContents);
await flushPromises();
const payload: ZclPayload = {
clusterID: apsFrame.clusterId,
header: Zcl.Header.fromBuffer(messageContents),
address: sender,
data: messageContents,
endpoint: apsFrame.sourceEndpoint,
linkquality: lastHopLqi,
groupID: apsFrame.groupId,
wasBroadcast: false,
destinationEndpoint: apsFrame.destinationEndpoint,
};
expect(spyResolveZCL).toHaveBeenCalledTimes(1);
expect(spyResolveZCL).toHaveBeenCalledWith(payload);
expect(spyEmit).toHaveBeenCalledWith("zclPayload", payload);
});
it("Emits ZCL payload on touchlink message", async () => {
const spyResolveZCL = vi.spyOn(
// @ts-expect-error private
adapter.oneWaitress,
"resolveZCL",
);
const spyEmit = vi.spyOn(adapter, "emit");
const sourcePanId: PanId = 0x1234;
const sourceAddress: Eui64 = "0x1122334455aabbcc";
const lastHopLqi = 252;
const groupId: number = 0;
const messageContents = Buffer.from("1803010000002003", "hex");
mockEzspEmitter.emit("touchlinkMessage", sourcePanId, sourceAddress, groupId, lastHopLqi, messageContents);
await flushPromises();
const payload: ZclPayload = {
clusterID: Zcl.Clusters.touchlink.ID,
header: Zcl.Header.fromBuffer(messageContents),
address: sourceAddress,
data: messageContents,
endpoint: 1,
linkquality: lastHopLqi,
groupID: groupId,
wasBroadcast: true,
destinationEndpoint: FIXED_ENDPOINTS[0].endpoint,
};
expect(spyResolveZCL).toHaveBeenCalledTimes(1);
expect(spyResolveZCL).toHaveBeenCalledWith(payload);
expect(spyEmit).toHaveBeenCalledWith("zclPayload", payload);
});
it("Emits ZCL payload on greenpower message", async () => {
const spyResolveZCL = vi.spyOn(
// @ts-expect-error private
adapter.oneWaitress,
"resolveZCL",
);
const spyEmit = vi.spyOn(adapter, "emit");
const sourceId: number = 1234;
const nwkAddress: NodeId = sourceId & 0xffff;
const gpdLink: number = 123;
const sequenceNumber: number = 1;
const commandIdentifier: number = Zcl.Clusters.greenPower.commands.commissioningNotification.ID;
const frameCounter: number = 102;
const gpdCommandId: number = 0xe0;
const gpdCommandPayload = Buffer.from([
0x02 /* deviceID */,
0x83 /* options */,
0xf2 /* extendedOptions */,
...[0xf1, 0xec, 0x92, 0xab, 0xff, 0x8f, 0x13, 0x63, 0xe1, 0x46, 0xbe, 0xb5, 0x18, 0xc9, 0x0c, 0xab] /* securityKey */,
0xa4,
0x46,
0xd4,
0xd5 /* keyMic */,
0xe4,
0x04,
0x00,
0x00 /* outgoingCounter */,
]);
const gpdHeader = Buffer.alloc(15);
gpdHeader.writeUInt8(0b00000001, 0);
gpdHeader.writeUInt8(sequenceNumber, 1);
gpdHeader.writeUInt8(commandIdentifier, 2);
gpdHeader.writeUInt16LE(0, 3);
gpdHeader.writeUInt32LE(sourceId, 5);
gpdHeader.writeUInt32LE(frameCounter, 9);
gpdHeader.writeUInt8(gpdCommandId, 13);
gpdHeader.writeUInt8(gpdCommandPayload.length, 14);
const messageContents = Buffer.concat([gpdHeader, gpdCommandPayload]);
const apsFrame: EmberApsFrame = {
profileId: ZSpec.GP_PROFILE_ID,
clusterId: Zcl.Clusters.greenPower.ID,
sourceEndpoint: ZSpec.GP_ENDPOINT,
destinationEndpoint: ZSpec.GP_ENDPOINT,
options: 0, // not used
groupId: ZSpec.GP_GROUP_ID,
sequence: 0, // not used
};
mockEzspEmitter.emit("incomingMessage", EmberIncomingMessageType.BROADCAST, apsFrame, gpdLink, nwkAddress, messageContents);
await flushPromises();
const payload: ZclPayload = {
header: Zcl.Header.fromBuffer(messageContents),
data: messageContents,
clusterID: apsFrame.clusterId,
address: nwkAddress,
endpoint: apsFrame.sourceEndpoint,
linkquality: gpdLink,
groupID: apsFrame.groupId,
wasBroadcast: true,
destinationEndpoint: apsFrame.destinationEndpoint,
};
expect(spyResolveZCL).toHaveBeenCalledTimes(1);
expect(spyResolveZCL).toHaveBeenCalledWith(payload);
expect(spyEmit).toHaveBeenCalledWith("zclPayload", payload);
});
it("Emits device joined on trust center join", async () => {
const spyEmit = vi.spyOn(adapter, "emit");
const newNodeId: NodeId = 1234;
const newNodeEui64: Eui64 = "0x11223344eebbccaa";
const status: EmberDeviceUpdate = EmberDeviceUpdate.STANDARD_SECURITY_UNSECURED_JOIN;
const policyDecision: EmberJoinDecision = EmberJoinDecision.USE_PRECONFIGURED_KEY;
const parentOfNewNodeId: NodeId = 4321;
mockEzspEmitter.emit("trustCenterJoin", newNodeId, newNodeEui64, status, policyDecision, parentOfNewNodeId);
await flushPromises();
expect(spyEmit).toHaveBeenCalledWith("deviceJoined", {
networkAddress: newNodeId,
ieeeAddr: newNodeEui64,
} as DeviceJoinedPayload);
});
it("Emits device leave on trust center join", async () => {
const spyEmit = vi.spyOn(adapter, "emit");
const newNodeId: NodeId = 1234;
const newNodeEui64: Eui64 = "0x11223344eebbccaa";
const status: EmberDeviceUpdate = EmberDeviceUpdate.DEVICE_LEFT;
const policyDecision: EmberJoinDecision = EmberJoinDecision.NO_ACTION;
const parentOfNewNodeId: NodeId = 0xffff;
mockEzspEmitter.emit("trustCenterJoin", newNodeId, newNodeEui64, status, policyDecision, parentOfNewNodeId);
await flushPromises();
expect(spyEmit).toHaveBeenCalledWith("deviceLeave", {
networkAddress: newNodeId,
ieeeAddr: newNodeEui64,
} as DeviceLeavePayload);
});
it("Handles DENY_JOIN on trust center join", async () => {
const newNodeId: NodeId = 1234;
const newNodeEui64: Eui64 = "0x11223344eebbccaa";
const status: EmberDeviceUpdate = EmberDeviceUpdate.STANDARD_SECURITY_UNSECURED_JOIN;
const policyDecision: EmberJoinDecision = EmberJoinDecision.DENY_JOIN;
const parentOfNewNodeId: NodeId = 4321;
mockEzspEmitter.emit("trustCenterJoin", newNodeId, newNodeEui64, status, policyDecision, parentOfNewNodeId);
await flushPromises();
expect(loggerSpies.warning).toHaveBeenCalledWith(
`[TRUST CENTER] Device ${newNodeId}:${newNodeEui64} was denied joining via ${parentOfNewNodeId}.`,
"zh:ember",
);
});
it("Handles device join workaround requiring specific manufacturer code", async () => {
const spyEmit = vi.spyOn(adapter, "emit");
const newNodeId: NodeId = 1234;
const newNodeEui64: Eui64 = "0x54ef44ffeebbccaa";
const status: EmberDeviceUpdate = EmberDeviceUpdate.STANDARD_SECURITY_UNSECURED_JOIN;
const policyDecision: EmberJoinDecision = EmberJoinDecision.USE_PRECONFIGURED_KEY;
const parentOfNewNodeId: NodeId = 4321;
mockEzspEmitter.emit("trustCenterJoin", newNodeId, newNodeEui64, status, policyDecision, parentOfNewNodeId);
await flushPromises();
expect(spyEmit).toHaveBeenCalledWith("deviceJoined", {
networkAddress: newNodeId,
ieeeAddr: newNodeEui64,
} as DeviceJoinedPayload);
expect(mockEzspSetManufacturerCode).toHaveBeenCalledWith(Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN);
expect(mockManufCode).toStrictEqual(Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN);
});
it("Triggers watchdog counters", async () => {
await vi.advanceTimersByTimeAsync(3610000);
expect(mockEzspReadAndClearCounters).toHaveBeenCalledTimes(1);
expect(loggerSpies.info).toHaveBeenCalledTimes(2);
expect(loggerSpies.info.mock.calls[0][0]).toMatch(/[NCP COUNTERS]/);
expect(loggerSpies.info.mock.calls[1][0]).toMatch(/[ASH COUNTERS]/);
});
it("Exports link keys", async () => {
const k1Context: SecManContext = {
coreKeyType: SecManKeyType.APP_LINK,
keyIndex: 0,
derivedType: SecManDerivedKeyType.NONE,
eui64: "0x1122334455667788",
multiNetworkIndex: 0,
flags: SecManFlag.EUI_IS_VALID | SecManFlag.KEY_INDEX_IS_VALID,
psaKeyAlgPermission: 0,
};
const k1 = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
const k1Hashed = ZSpec.Utils.aes128MmoHash(k1);
const k1Metadata: SecManAPSKeyMetadata = {
bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER,
outgoingFrameCounter: 1,
incomingFrameCounter: 2,
ttlInSeconds: 0,
};
const k2Context: SecManContext = {
coreKeyType: SecManKeyType.APP_LINK,
keyIndex: 1,
derivedType: SecManDerivedKeyType.NONE,
eui64: "0x2233445566778899",
multiNetworkIndex: 0,
flags: SecManFlag.EUI_IS_VALID | SecManFlag.KEY_INDEX_IS_VALID,
psaKeyAlgPermission: 0,
};
const k2 = Buffer.from([2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]);
const k2Hashed = ZSpec.Utils.aes128MmoHash(k2);
const k2Metadata: SecManAPSKeyMetadata = {
bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER,
outgoingFrameCounter: 10,
incomingFrameCounter: 20,
ttlInSeconds: 0,
};
const k3Context: SecManContext = {
coreKeyType: SecManKeyType.APP_LINK,
keyIndex: 2,
derivedType: SecManDerivedKeyType.NONE,
eui64: "0x3344556677889900",
multiNetworkIndex: 0,
flags: SecManFlag.EUI_IS_VALID | SecManFlag.KEY_INDEX_IS_VALID,
psaKeyAlgPermission: 0,
};
const k3 = Buffer.from([3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]);
const k3Hashed = ZSpec.Utils.aes128MmoHash(k3);
const k3Metadata: SecManAPSKeyMetadata = {
bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER,
outgoingFrameCounter: 100,
incomingFrameCounter: 200,
ttlInSeconds: 0,
};
mockEzspGetConfigurationValue.mockResolvedValueOnce([SLStatus.OK, 3]);
mockEzspExportLinkKeyByIndex
.mockResolvedValueOnce([SLStatus.OK, k1Context, {contents: k1} as SecManKey, k1Metadata])
.mockResolvedValueOnce([SLStatus.OK, k2Context, {contents: k2} as SecManKey, k2Metadata])
.mockResolvedValueOnce([SLStatus.OK, k3Context, {contents: k3} as SecManKey, k3Metadata]);
const keys = await adapter.exportLinkKeys();
expect(mockEzspExportLinkKeyByIndex).toHaveBeenCalledTimes(3);
expect(keys).toStrictEqual([
{
deviceEui64: k1Context.eui64,
key: {contents: k1Hashed},
outgoingFrameCounter: k1Metadata.outgoingFrameCounter,
incomingFrameCounter: k1Metadata.incomingFrameCounter,
} as LinkKeyBackupData,
{
deviceEui64: k2Context.eui64,
key: {contents: k2Hashed},
outgoingFrameCounter: k2Metadata.outgoingFrameCounter,
incomingFrameCounter: k2Metadata.incomingFrameCounter,
} as LinkKeyBackupData,
{
deviceEui64: k3Context.eui64,
key: {contents: k3Hashed},
outgoingFrameCounter: k3Metadata.outgoingFrameCounter,
incomingFrameCounter: k3Metadata.incomingFrameCounter,
} as LinkKeyBackupData,
]);
});
it("Exports zero link keys", async () => {
mockEzspGetConfigurationValue.mockResolvedValueOnce([SLStatus.OK, 0]);
const keys = await adapter.exportLinkKeys();
expect(keys).toStrictEqual([]);
});
it("Fails to export link keys due to failed table size retrieval", async () => {
mockEzspGetConfigurationValue.mockResolvedValueOnce([SLStatus.FAIL, 0]);
await expect(adapter.exportLinkKeys()).rejects.toThrow("[BACKUP] Failed to retrieve key table size from NCP with status=FAIL.");
});
it("Imports link keys", async () => {
const k1Context: SecManContext = {
coreKeyType: SecManKeyType.APP_LINK,
keyIndex: 0,
derivedType: SecManDerivedKeyType.NONE,
eui64: "0x1122334455667788",
multiNetworkIndex: 0,
flags: SecManFlag.EUI_IS_VALID | SecManFlag.KEY_INDEX_IS_VALID,
psaKeyAlgPermission: 0,
};
const k1 = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
const k1Metadata: SecManAPSKeyMetadata = {
bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER,
outgoingFrameCounter: 1,
incomingFrameCounter: 2,
ttlInSeconds: 0,
};
const k2Context: SecManContext = {
coreKeyType: SecManKeyType.APP_LINK,
keyIndex: 1,
derivedType: SecManDerivedKeyType.NONE,
eui64: "0x2233445566778899",
multiNetworkIndex: 0,
flags: SecManFlag.EUI_IS_VALID | SecManFlag.KEY_INDEX_IS_VALID,
psaKeyAlgPermission: 0,
};
const k2 = Buffer.from([2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]);
const k2Metadata: SecManAPSKeyMetadata = {
bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER,
outgoingFrameCounter: 10,
incomingFrameCounter: 20,
ttlInSeconds: 0,
};
const k3Context: SecManContext = {
coreKeyType: SecManKeyType.APP_LINK,
keyIndex: 2,
derivedType: SecManDerivedKeyType.NONE,
eui64: "0x3344556677889900",
multiNetworkIndex: 0,
flags: SecManFlag.EUI_IS_VALID | SecManFlag.KEY_INDEX_IS_VALID,
psaKeyAlgPermission: 0,
};
const k3 = Buffer.from([3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]);
const k3Metadata: SecManAPSKeyMetadata = {
bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER,
outgoingFrameCounter: 100,
incomingFrameCounter: 200,
ttlInSeconds: 0,
};
mockEzspGetConfigurationValue.mockResolvedValueOnce([SLStatus.OK, 4]);
mockEzspNetworkState.mockResolvedValueOnce(EmberNetworkStatus.NO_NETWORK);
await adapter.importLinkKeys([
{
deviceEui64: k1Context.eui64,
key: {contents: k1},
outgoingFrameCounter: k1Metadata.outgoingFrameCounter,
incomingFrameCounter: k1Metadata.incomingFrameCounter,
},
{
deviceEui64: k2Context.eui64,
key: {contents: k2},
outgoingFrameCounter: k2Metadata.outgoingFrameCounter,
incomingFrameCounter: k2Metadata.incomingFrameCounter,
},
{
deviceEui64: k3Context.eui64,
key: {contents: k3},
outgoingFrameCounter: k3Metadata.outgoingFrameCounter,
incomingFrameCounter: k3Metadata.incomingFrameCounter,
},
]);
expect(mockEzspImportLinkKey).toHaveBeenCalledTimes(3);
expect(mockEzspEraseKeyTableEntry).toHaveBeenCalledTimes(1);
});
it("Imports zero link keys", async () => {
await expect(adapter.importLinkKeys([])).resolves.toStrictEqual(undefined);
});
it("Failed to import link keys due to failed table size retrieval", async () => {
mockEzspGetConfigurationValue.mockResolvedValueOnce([SLStatus.FAIL, 0]);
await expect(
adapter.importLinkKeys([
// @ts-expect-error mock, unnecessary
{},
]),
).rejects.toThrow("[BACKUP] Failed to retrieve key table size from NCP with status=FAIL.");
});
it("Failed to import link keys due to insufficient table size", async () => {
mockEzspGetConfigurationValue.mockResolvedValueOnce([SLStatus.OK, 0]);
await expect(
adapter.importLinkKeys([
// @ts-expect-error mock, unnecessary
{},
]),
).rejects.toThrow("[BACKUP] Current key table of 0 is too small to import backup of 1!");
});
it("Failed to import link keys due to improper network state", async () => {
mockEzspGetConfigurationValue.mockResolvedValueOnce([SLStatus.OK, 3]);
mockEzspNetworkState.mockResolvedValueOnce(EmberNetworkStatus.JOINED_NETWORK);
await expect(
adapter.importLinkKeys([
// @ts-expect-error mock, unnecessary
{},
]),
).rejects.toThrow(
`[BACKUP] Cannot import TC data while network is up, networkStatus=${EmberNetworkStatus[EmberNetworkStatus.JOINED_NETWORK]}.`,
);
});
it("Failed to import link keys due to failed key set", async () => {
const k1Context: SecManContext = {
coreKeyType: SecManKeyType.APP_LINK,
keyIndex: 0,
derivedType: SecManDerivedKeyType.NONE,
eui64: "0x1122334455667788",
multiNetworkIndex: 0,
flags: SecManFlag.EUI_IS_VALID | SecManFlag.KEY_INDEX_IS_VALID,
psaKeyAlgPermission: 0,
};
const k1 = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
const k1Metadata: SecManAPSKeyMetadata = {
bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER,
outgoingFrameCounter: 1,
incomingFrameCounter: 2,
ttlInSeconds: 0,
};
mockEzspGetConfigurationValue.mockResolvedValueOnce([SLStatus.OK, 3]);
mockEzspNetworkState.mockResolvedValueOnce(EmberNetworkStatus.NO_NETWORK);
mockEzspImportLinkKey.mockResolvedValueOnce(SLStatus.FAIL);
await expect(
adapter.importLinkKeys([
{
deviceEui64: k1Context.eui64,
key: {contents: k1},
outgoingFrameCounter: k1Metadata.outgoingFrameCounter,
incomingFrameCounter: k1Metadata.incomingFrameCounter,
},
]),
).rejects.toThrow("[BACKUP] Failed to set key table entry at index 0 with status=FAIL.");
});
it("Failed to import link keys due to failed key erase", async () => {
const k1Context: SecManContext = {
coreKeyType: SecManKeyType.APP_LINK,
keyIndex: 0,
derivedType: SecManDerivedKeyType.NONE,
eui64: "0x1122334455667788",
multiNetworkIndex: 0,
flags: SecManFlag.EUI_IS_VALID | SecManFlag.KEY_INDEX_IS_VALID,
psaKeyAlgPermission: 0,
};
const k1 = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
const k1Metadata: SecManAPSKeyMetadata = {
bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER,
outgoingFrameCounter: 1,
incomingFrameCounter: 2,
ttlInSeconds: 0,
};
mockEzspGetConfigurationValue.mockResolvedValueOnce([SLStatus.OK, 3]);
mockEzspNetworkState.mockResolvedValueOnce(EmberNetworkStatus.NO_NETWORK);
mockEzspEraseKeyTableEntry.mockResolvedValueOnce(SLStatus.FAIL);
await expect(
adapter.importLinkKeys([
{
deviceEui64: k1Context.eui64,
key: {contents: k1},
outgoingFrameCounter: k1Metadata.outgoingFrameCounter,
incomingFrameCounter: k1Metadata.incomingFrameCounter,
},
]),
).rejects.toThrow("[BACKUP] Failed to erase key table entry at index 1 with status=FAIL.");
});
it("Broadcasts network key update", async () => {
const p = adapter.broadcastNetworkKeyUpdate();
await vi.advanceTimersByTimeAsync(100000);
await expect(p).resolves.toStrictEqual(undefined);
expect(mockEzspBroadcastNextNetworkKey).toHaveBeenCalledTimes(1);
expect(mockEzspBroadcastNetworkKeySwitch).toHaveBeenCalledTimes(1);
});
it("Fails to broadcast network key update due to failed next key broadcast", async () => {
mockEzspBroadcastNextNetworkKey.mockResolvedValueOnce(SLStatus.FAIL);
const p = defuseRejection(adapter.broadcastNetworkKeyUpdate());
await vi.advanceTimersByTimeAsync(100000);
await expect(p).rejects.toThrow("[TRUST CENTER] Failed to broadcast next network key with status=FAIL.");
expect(mockEzspBroadcastNextNetworkKey).toHaveBeenCalledTimes(1);
expect(mockEzspBroadcastNetworkKeySwitch).toHaveBeenCalledTimes(0);
});
it("Fails to broadcast network key update due to failed switch broadcast", async () => {
mockEzspBroadcastNetworkKeySwitch.mockResolvedValueOnce(SLStatus.FAIL);
const p = defuseRejection(adapter.broadcastNetworkKeyUpdate());
await vi.advanceTimersByTimeAsync(100000);
await expect(p).rejects.toThrow("[TRUST CENTER] Failed to broadcast network key switch with status=FAIL.");
expect(mockEzspBroadcastNextNetworkKey).toHaveBeenCalledTimes(1);
expect(mockEzspBroadcastNetworkKeySwitch).toHaveBeenCalledTimes(1);
});
it("Handles NCP needing reset & init", async () => {
const spyEmit = vi.spyOn(adapter, "emit");
mockEzspEmitter.emit("ncpNeedsResetAndInit", EzspStatus.ERROR_SERIAL_INIT);
await vi.advanceTimersByTimeAsync(5000);
expect(spyEmit).toHaveBeenCalledTimes(1);
expect(spyEmit).toHaveBeenCalledWith("disconnected");
});
it("Emits adapter disconnected when NCP needs reset & init but queue is too high", async () => {
vi.spyOn(
// @ts-expect-error private
adapter.queue,
"count",
).mockReturnValueOnce(999);
const spyEmit = vi.spyOn(adapter, "emit");
mockEzspEmitter.emit("ncpNeedsResetAndInit", EzspStatus.ERROR_SERIAL_INIT);
await flushPromises();
expect(spyEmit).toHaveBeenCalledWith("disconnected");
});
it("Emits adapter disconnected when failed to reset & init NCP", async () => {
vi.spyOn(adapter, "stop").mockRejectedValueOnce(new Error("mock error"));
const spyEmit = vi.spyOn(adapter, "emit");
mockEzspEmitter.emit("ncpNeedsResetAndInit", EzspStatus.ERROR_SERIAL_INIT);
await flushPromises();
expect(spyEmit).toHaveBeenCalledWith("disconnected");
});
it("Handles channel changed stack status", async () => {
mockEzspEmitter.emit("stackStatus", SLStatus.ZIGBEE_CHANNEL_CHANGED);
await flushPromises();
expect(loggerSpies.info).toHaveBeenCalledWith("[STACK STATUS] Channel changed.", "zh:ember");
});
it.each([
["getCoordinatorIEEE", []],
["getNetworkParameters", []],
["permitJoin", [250, 1234]],
["permitJoin", [250]],
[
"sendZclFrameToEndpoint",
[
"0x1122334455667788",
1234,
1,
Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, undefined, 1, 1, 0, [{}], {}),
10000,
true,
false,
1,
],
],
[
"sendZclFrameToGroup",
[32, Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, undefined, 1, 1, 0, [{}], {}), 1],
],
[
"sendZclFrameToAll",
[
1,
Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, undefined, 1, 1, 0, [{}], {}),
1,
ZSpec.BroadcastAddress.DEFAULT,
],
],
])("Adapter impl: throws when using non-InterPAN function %s while in InterPAN mode", async (funcName, args) => {
await adapter.setChannelInterPAN(15);
await expect(
// @ts-expect-error mock
adapter[funcName](...args),
).rejects.toThrow("[INTERPAN MODE] Cannot execute non-InterPAN commands.");
});
it("Adapter impl: getCoordinatorIEEE", async () => {
await expect(adapter.getCoordinatorIEEE()).resolves.toStrictEqual(DEFAULT_COORDINATOR_IEEE);
});
it("Adapter impl: getCoordinatorVersion", async () => {
await expect(adapter.getCoordinatorVersion()).resolves.toStrictEqual({
type: "EmberZNet",
meta: {
ezsp: EZSP_PROTOCOL_VERSION,
revision: `8.0.0 [${EmberVersionType[EmberVersionType.GA]}]`,
build: 135,
major: 8,
minor: 0,
patch: 0,
special: 0,
type: EmberVersionType.GA,
},
} as TsType.CoordinatorVersion);
});
it("Adapter impl: reset soft", async () => {
await expect(adapter.reset("soft")).rejects.toThrow(`Not supported 'soft'.`);
});
it("Adapter impl: reset hard", async () => {
await expect(adapter.reset("hard")).rejects.toThrow(`Not supported 'hard'.`);
});
it("Adapter impl: supportsBackup", async () => {
await expect(adapter.supportsBackup()).resolves.toStrictEqual(true);
});
it("Adapter impl: backup", async () => {
await expect(adapter.backup([])).resolves.toStrictEqual({
networkOptions: {
panId: DEFAULT_NETWORK_OPTIONS.panID, // uint16_t
extendedPanId: Buffer.from(DEFAULT_NETWORK_OPTIONS.extendedPanID!),
channelList: ZSpec.ALL_802_15_4_CHANNELS.slice(),
networkKey: Buffer.from(DEFAULT_BACKUP.network_key.key, "hex"),
networkKeyDistribute: false,
},
logicalChannel: DEFAULT_NETWORK_OPTIONS.channelList[0],
networkKeyInfo: {
sequenceNumber: DEFAULT_BACKUP.network_key.sequence_number,
frameCounter: DEFAULT_BACKUP.network_key.frame_counter,
},
securityLevel: SECURITY_LEVEL_Z3,
networkUpdateId: 0,
coordinatorIeeeAddress: Buffer.from(DEFAULT_BACKUP.coordinator_ieee, "hex"),
devices: [],
ezsp: {
version: EZSP_PROTOCOL_VERSION,
hashed_tclk: Buffer.from(DEFAULT_BACKUP.stack_specific!.ezsp!.hashed_tclk!, "hex"),
},
} as Backup);
});
it.each([
[
"failed get network parameters",
() => {
mockEzspGetNetworkParameters.mockResolvedValueOnce([SLStatus.FAIL, 0, {}]);
},
"[BACKUP] Failed to get network parameters with status=FAIL.",
],
[
"failed get network key info",
() => {
mockEzspGetNetworkKeyInfo.mockResolvedValueOnce([SLStatus.FAIL, {}]);
},
"[BACKUP] Failed to get network keys info with status=FAIL.",
],
// [
// 'failed get TC APS key info',
// () => {
// mockEzspGetNetworkKeyInfo.mockResolvedValueOnce([SLStatus.FAIL, {}]);
// },
// `[BACKUP] Failed to get TC APS key info with status=FAIL.`,
// ],
[
"no network key set",
() => {
mockEzspGetNetworkKeyInfo.mockResolvedValueOnce([
SLStatus.OK,
{
networkKeySet: false,
alternateNetworkKeySet: false,
networkKeySequenceNumber: 123,
altNetworkKeySequenceNumber: 0,
networkKeyFrameCounter: 456,
} as SecManNetworkKeyInfo,
]);
},
"[BACKUP] No network key set.",
],
[
"failed export TC link key",
() => {
mockEzspExportKey.mockResolvedValueOnce([SLStatus.FAIL, {}]);
},
"[BACKUP] Failed to export TC Link Key with status=FAIL.",
],
[
"failed export network key",
() => {
mockEzspExportKey
.mockResolvedValueOnce([
SLStatus.OK,
{contents: Buffer.from(DEFAULT_BACKUP.stack_specific!.ezsp!.hashed_tclk!, "hex")} as SecManKey,
])
.mockResolvedValueOnce([SLStatus.FAIL, {}]);
},
"[BACKUP] Failed to export Network Key with status=FAIL.",
],
])("Adapter impl: throws when backup fails due to %s", async (_command, setup, error) => {
setup();
await expect(adapter.backup([])).rejects.toThrow(error);
});
it("Adapter impl: getNetworkParameters from cache", async () => {
await expect(adapter.getNetworkParameters()).resolves.toStrictEqual({
panID: DEFAULT_NETWORK_OPTIONS.panID,
extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(DEFAULT_NETWORK_OPTIONS.extendedPanID!)),
channel: DEFAULT_NETWORK_OPTIONS.channelList[0],
nwkUpdateID: 0,
} as TsType.NetworkParameters);
expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(0);
});
it("Adapter impl: getNetworkParameters from NCP", async () => {
adapter.clearNetworkCache();
await expect(adapter.getNetworkParameters()).resolves.toStrictEqual({
panID: DEFAULT_NETWORK_OPTIONS.panID,
extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(DEFAULT_NETWORK_OPTIONS.extendedPanID!)),
channel: DEFAULT_NETWORK_OPTIONS.channelList[0],
nwkUpdateID: 0,
} as TsType.NetworkParameters);
expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(1);
});
it("Adapter impl: addInstallCode", async () => {
await expect(
adapter.addInstallCode("0x1122334455667788", Buffer.from("DD7ED5CDAA8E2C708B67D2B1573DB6843A5F", "hex"), false),
).resolves.toStrictEqual(undefined);
expect(mockEzspImportTransientKey).toHaveBeenCalledTimes(1);
expect(loggerSpies.debug).toHaveBeenCalledWith(`[ADD INSTALL CODE] Success for '0x1122334455667788'.`, "zh:ember");
});
it("Adapter impl: throw when addInstallCode fails import transient key", async () => {
mockEzspImportTransientKey.mockResolvedValueOnce(SLStatus.FAIL);
await expect(adapter.addInstallCode("0x1122334455667788", Buffer.alloc(16), true)).rejects.toThrow(
`[ADD INSTALL CODE] Failed for '0x1122334455667788' with status=FAIL.`,
);
expect(mockEzspImportTransientKey).toHaveBeenCalledTimes(1);
});
it("Adapter impl: waitFor", () => {
const waiter = adapter.waitFor(1234, 1, Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, 10, 0, 1, 15000);
const spyCancel = vi.spyOn(waiter, "cancel");
expect(typeof waiter.cancel).toStrictEqual("function");
expect(waiter.promise).toBeDefined();
waiter.cancel();
expect(spyCancel).toHaveReturned();
});
it("Adapter impl: permitJoin on all", async () => {
const spyResolveEvent = vi.spyOn(
// @ts-expect-error private
adapter.oneWaitress,
"resolveEvent",
);
await adapter.permitJoin(250);
await vi.advanceTimersByTimeAsync(1000);
expect(mockEzspPermitJoining).toHaveBeenCalledWith(250);
expect(mockEzspSendBroadcast).toHaveBeenCalledTimes(1);
expect(spyResolveEvent).toHaveBeenCalledWith(OneWaitressEvents.STACK_STATUS_NETWORK_OPENED);
await adapter.permitJoin(0);
await vi.advanceTimersByTimeAsync(1000);
expect(mockEzspPermitJoining).toHaveBeenCalledWith(0);
expect(mockEzspSendBroadcast).toHaveBeenCalledTimes(2);
expect(spyResolveEvent).toHaveBeenCalledWith(OneWaitressEvents.STACK_STATUS_NETWORK_CLOSED);
expect(mockEzspSetPolicy).toHaveBeenNthCalledWith(
1,
EzspPolicyId.TRUST_CENTER_POLICY,
EzspDecisionBitmask.ALLOW_JOINS | EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS,
);
expect(mockEzspSetPolicy).toHaveBeenNthCalledWith(2, EzspPolicyId.TRUST_CENTER_POLICY, EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS);
});
it("Adapter impl: permitJoin on coordinator", async () => {
const spyResolveEvent = vi.spyOn(
// @ts-expect-error private
adapter.oneWaitress,
"resolveEvent",
);
await adapter.permitJoin(250, ZSpec.COORDINATOR_ADDRESS);
await vi.advanceTimersByTimeAsync(1000);
expect(mockEzspPermitJoining).toHaveBeenCalledWith(250);
expect(mockEzspSendBroadcast).toHaveBeenCalledTimes(0);
expect(spyResolveEvent).toHaveBeenCalledWith(OneWaitressEvents.STACK_STATUS_NETWORK_OPENED);
await adapter.permitJoin(0, ZSpec.COORDINATOR_ADDRESS);
await vi.advanceTimersByTimeAsync(1000);
expect(mockEzspPermitJoining).toHaveBeenCalledWith(0);
expect(mockEzspSendBroadcast).toHaveBeenCalledTimes(0);
expect(spyResolveEvent).toHaveBeenCalledWith(OneWaitressEvents.STACK_STATUS_NETWORK_CLOSED);
expect(mockEzspSetPolicy).toHaveBeenNthCalledWith(
1,
EzspPolicyId.TRUST_CENTER_POLICY,
EzspDecisionBitmask.ALLOW_JOINS | EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS,
);
expect(mockEzspSetPolicy).toHaveBeenNthCalledWith(2, EzspPolicyId.TRUST_CENTER_POLICY, EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS);
});
it("Adapter impl: permitJoin on router", async () => {
const spyResolveZDO = vi.spyOn(
// @ts-expect-error private
adapter.oneWaitress,
"resolveZDO",
);
const sender = 1234;
const apsFrame: EmberApsFrame = {
profileId: Zdo.ZDO_PROFILE_ID,
clusterId: Zdo.ClusterId.PERMIT_JOINING_RESPONSE,
sourceEndpoint: Zdo.ZDO_ENDPOINT,
destinationEndpoint: Zdo.ZDO_ENDPOINT,
options: 0,
groupId: 0,
sequence: 0,
};
const emitResponse = () => {
setTimeout(async () => {
mockEzspEmitter.emit("zdoResponse", apsFrame, sender, Buffer.from([1, Zdo.Status.SUCCESS]));
await flushPromises();
}, 300);
return [SLStatus.OK, ++mockAPSSequence];
};
mockEzspSendUnicast.mockImplementationOnce(emitResponse).mockImplementationOnce(emitResponse);
const zdoResponse = [Zdo.Status.SUCCESS, undefined];
let p = adapter.permitJoin(250, sender);
await vi.advanceTimersByTimeAsync(1000);
await p;
expect(mockEzspSendUnicast).toHaveBeenCalledTimes(1);
expect(spyResolveZDO).toHaveBeenCalledTimes(1);
expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, zdoResponse);
p = adapter.permitJoin(0, sender);
await vi.advanceTimersByTimeAsync(1000);
await p;
expect(mockEzspSendUnicast).toHaveBeenCalledTimes(2);
expect(spyResolveZDO).toHaveBeenCalledTimes(2);
expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, zdoResponse);
expect(mockEzspSetPolicy).toHaveBeenNthCalledWith(
1,
EzspPolicyId.TRUST_CENTER_POLICY,
EzspDecisionBitmask.ALLOW_JOINS | EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS,
);
expect(mockEzspSetPolicy).toHaveBeenNthCalledWith(2, EzspPolicyId.TRUST_CENTER_POLICY, EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS);
});
it("Adapter impl: permitJoin restores temp manufacturer code", async () => {
const spyResolveEvent = vi.spyOn(
// @ts-expect-error private
adapter.oneWaitress,
"resolveEvent",
);
const newNodeId: NodeId = 1234;
const newNodeEui64: Eui64 = "0x54ef44ffeebbccaa";
const status: EmberDeviceUpdate = EmberDeviceUpdate.STANDARD_SECURITY_UNSECURED_JOIN;
const policyDecision: EmberJoinDecision = EmberJoinDecision.USE_PRECONFIGURED_KEY;
const parentOfNewNodeId: NodeId = 4321;
mockEzspEmitter.emit("trustCenterJoin", newNodeId, newNodeEui64, status, policyDecision, parentOfNewNodeId);
await flushPromises();
expect(mockEzspSetManufacturerCode).toHaveBeenCalledWith(Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN);
expect(mockManufCode).toStrictEqual(Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN);
await adapter.permitJoin(0, ZSpec.COORDINATOR_ADDRESS);
await vi.advanceTimersByTimeAsync(1000);
expect(mockEzspPermitJoining).toHaveBeenCalledWith(0);
expect(mockEzspSendBroadcast).toHaveBeenCalledTimes(0);
expect(spyResolveEvent).toHaveBeenCalledWith(OneWaitressEvents.STACK_STATUS_NETWORK_CLOSED);
expect(mockEzspSetManufacturerCode).toHaveBeenCalledWith(Zcl.ManufacturerCode.SILICON_LABORATORIES);
expect(mockManufCode).toStrictEqual(Zcl.ManufacturerCode.SILICON_LABORATORIES);
});
it("Adapter impl: throws when permitJoin request on coordinator fails", async () => {
mockEzspPermitJoining.mockResolvedValueOnce(SLStatus.FAIL);
await expect(adapter.permitJoin(250, 0)).rejects.toThrow("[ZDO] Failed coordinator permit joining request with status=FAIL.");
});
it("Adapter impl: throws when permitJoin broadcast request fails", async () => {
mockEzspSendBroadcast.mockResolvedValueOnce([SLStatus.FAIL, 0]);
await expect(defuseRejection(adapter.permitJoin(250, undefined))).rejects.toThrow(
"~x~> [ZDO PERMIT_JOINING_REQUEST BROADCAST to=65532 messageTag=1] Failed to send request with status=FAIL.",
);
});
it("Adapter impl: resolves undefined when permitJoin on router fails due to failed ZDO status", async () => {
const spyResolveZDO = vi.spyOn(
// @ts-expect-error private
adapter.oneWaitress,
"resolveZDO",
);
const sender = 1234;
const apsFrame: EmberApsFrame = {
profileId: Zdo.ZDO_PROFILE_ID,
clusterId: Zdo.ClusterId.PERMIT_JOINING_RESPONSE,
sourceEndpoint: Zdo.ZDO_ENDPOINT,
destinationEndpoint: Zdo.ZDO_ENDPOINT,
options: 0,
groupId: 0,
sequence: 0,
};
mockEzspEmitter.emit("zdoResponse", apsFrame, sender, Buffer.from([1, Zdo.Status.NOT_AUTHORIZED]));
await flushPromises();
const zdoResponse = [Zdo.Status.NOT_AUTHORIZED, undefined];
expect(spyResolveZDO).toHaveBeenCalledTimes(1);
expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, zdoResponse);
});
it("Adapter impl: throws when permitJoin request on router fails", async () => {
mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]);
await expect(adapter.permitJoin(250, 1234)).rejects.toThrow(
"~x~> [ZDO PERMIT_JOINING_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.",
);
});
it("Adapter impl: throws when permitJoin fails to import ZIGBEE_PROFILE_INTEROPERABILITY_LINK_KEY", async () => {
mockEzspImportTransientKey.mockResolvedValueOnce(SLStatus.FAIL);
await expect(adapter.permitJoin(250)).rejects.toThrow("[ZDO] Failed import transient key with status=FAIL.");
});
it("Adapter impl: throws when permitJoin fails to set TC policy", async () => {
mockEzspSetPolicy.mockResolvedValueOnce(SLStatus.FAIL);
await expect(adapter.permitJoin(250)).rejects.toThrow("[ZDO] Failed set join policy with status=FAIL.");
});
it("Adapter impl: throws when stop permitJoin fails to restore TC policy", async () => {
mockEzspSetPolicy.mockResolvedValueOnce(SLStatus.FAIL);
await expect(adapter.permitJoin(0)).rejects.toThrow("[ZDO] Failed set join policy with status=FAIL.");
});
it("Adapter impl: sendZclFrameToEndpoint with command response with fixed source endpoint", async () => {
const networkAddress: NodeId = 1234;
const endpoint: number = 1;
const sourceEndpoint = FIXED_ENDPOINTS[0].endpoint;
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: 0,
sequence: 0, // set by stack
};
const lastHopLqi: number = 234;
// Received Zigbee message from '0x', type 'readResponse', cluster 'genBasic', data '{"zclVersion":3}' from endpoint 1 with groupID 0
const messageContents = Buffer.from("1803010000002003", "hex");
mockEzspSend.mockImplementationOnce(() => {
setTimeout(async () => {
mockEzspEmitter.emit(
"incomingMessage",
EmberIncomingMessageType.UNICAST,
reverseApsFrame(apsFrame),
lastHopLqi,
networkAddress,
messageContents,
);
await flushPromises();
}, 300);
return [SLStatus.OK, ++mockAPSSequence];
});
const p = adapter.sendZclFrameToEndpoint("0x1122334455667788", networkAddress, endpoint, zclFrame, 10000, false, false, sourceEndpoint);
await vi.advanceTimersByTimeAsync(5000);
await expect(p).resolves.toStrictEqual({
clusterID: apsFrame.clusterId,
header: Zcl.Header.fromBuffer(messageContents),
address: networkAddress,
data: messageContents,
endpoint: apsFrame.destinationEndpoint,
linkquality: lastHopLqi,
groupID: apsFrame.groupId,
wasBroadcast: false,
destinationEndpoint: apsFrame.sourceEndpoint,
} as ZclPayload);
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it("Adapter impl: sendZclFrameToEndpoint with command response with other source endpoint", async () => {
const networkAddress: NodeId = 1234;
const endpoint: number = 1;
const sourceEndpoint = 3;
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: 0,
sequence: 0, // set by stack
};
const lastHopLqi: number = 234;
// Received Zigbee message from '0x', type 'readResponse', cluster 'genBasic', data '{"zclVersion":3}' from endpoint 1 with groupID 0
const messageContents = Buffer.from("1803010000002003", "hex");
mockEzspSend.mockImplementationOnce(() => {
setTimeout(async () => {
mockEzspEmitter.emit(
"incomingMessage",
EmberIncomingMessageType.UNICAST,
reverseApsFrame(apsFrame),
lastHopLqi,
networkAddress,
messageContents,
);
await flushPromises();
}, 300);
return [SLStatus.OK, ++mockAPSSequence];
});
const p = adapter.sendZclFrameToEndpoint("0x1122334455667788", networkAddress, endpoint, zclFrame, 10000, false, false, sourceEndpoint);
await vi.advanceTimersByTimeAsync(5000);
await expect(p).resolves.toStrictEqual({
clusterID: apsFrame.clusterId,
header: Zcl.Header.fromBuffer(messageContents),
address: networkAddress,
data: messageContents,
endpoint: apsFrame.destinationEndpoint,
linkquality: lastHopLqi,
groupID: apsFrame.groupId,
wasBroadcast: false,
destinationEndpoint: apsFrame.sourceEndpoint,
} as ZclPayload);
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it("Adapter impl: sendZclFrameToEndpoint with command response with no source endpoint", async () => {
const networkAddress: NodeId = 1234;
const endpoint: number = 1;
const sourceEndpoint = FIXED_ENDPOINTS[0].endpoint;
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: 0,
sequence: 0, // set by stack
};
const lastHopLqi: number = 234;
// Received Zigbee message from '0x', type 'readResponse', cluster 'genBasic', data '{"zclVersion":3}' from endpoint 1 with groupID 0
const messageContents = Buffer.from("1803010000002003", "hex");
mockEzspSend.mockImplementationOnce(() => {
setTimeout(async () => {
mockEzspEmitter.emit(
"incomingMessage",
EmberIncomingMessageType.UNICAST,
reverseApsFrame(apsFrame),
lastHopLqi,
networkAddress,
messageContents,
);
await flushPromises();
}, 300);
return [SLStatus.OK, ++mockAPSSequence];
});
const p = adapter.sendZclFrameToEndpoint("0x1122334455667788", networkAddress, endpoint, zclFrame, 10000, false, false);
await vi.advanceTimersByTimeAsync(5000);
await expect(p).resolves.toStrictEqual({
clusterID: apsFrame.clusterId,
header: Zcl.Header.fromBuffer(messageContents),
address: networkAddress,
data: messageContents,
endpoint: apsFrame.destinationEndpoint,
linkquality: lastHopLqi,
groupID: apsFrame.groupId,
wasBroadcast: false,
destinationEndpoint: apsFrame.sourceEndpoint,
} as ZclPayload);
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it.each([
["NO_TX_SPACE", EzspStatus.NO_TX_SPACE],
["NOT_CONNECTED", EzspStatus.NOT_CONNECTED],
])("Adapter impl: recovers when sendZclFrameToEndpoint throws %s status", async (_statusName, status) => {
const networkAddress: NodeId = 1234;
const endpoint: number = 1;
const sourceEndpoint = FIXED_ENDPOINTS[0].endpoint;
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: 0,
sequence: 0, // set by stack
};
const lastHopLqi: number = 234;
// Received Zigbee message from '0x', type 'readResponse', cluster 'genBasic', data '{"zclVersion":3}' from endpoint 1 with groupID 0
const messageContents = Buffer.from("1803010000002003", "hex");
mockEzspSend.mockRejectedValueOnce(new EzspError(status)).mockImplementationOnce(() => {
setTimeout(async () => {
mockEzspEmitter.emit(
"incomingMessage",
EmberIncomingMessageType.UNICAST,
reverseApsFrame(apsFrame),
lastHopLqi,
networkAddress,
messageContents,
);
await flushPromises();
}, 300);
return [SLStatus.OK, ++mockAPSSequence];
});
const p = adapter.sendZclFrameToEndpoint("0x1122334455667788", networkAddress, endpoint, zclFrame, 10000, false, false, sourceEndpoint);
await vi.advanceTimersByTimeAsync(10000);
await expect(p).resolves.toStrictEqual({
clusterID: apsFrame.clusterId,
header: Zcl.Header.fromBuffer(messageContents),
address: networkAddress,
data: messageContents,
endpoint: apsFrame.destinationEndpoint,
linkquality: lastHopLqi,
groupID: apsFrame.groupId,
wasBroadcast: false,
destinationEndpoint: apsFrame.sourceEndpoint,
} as ZclPayload);
expect(mockEzspSend).toHaveBeenCalledTimes(2);
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it.each([
["ZIGBEE_MAX_MESSAGE_LIMIT_REACHED", SLStatus.ZIGBEE_MAX_MESSAGE_LIMIT_REACHED],
["BUSY", SLStatus.BUSY],
["NETWORK_DOWN", SLStatus.NETWORK_DOWN],
])("Adapter impl: recovers when sendZclFrameToEndpoint get %s status from NCP", async (_statusName, status) => {
const networkAddress: NodeId = 1234;
const endpoint: number = 1;
const sourceEndpoint = FIXED_ENDPOINTS[0].endpoint;
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: 0,
sequence: 0, // set by stack
};
const lastHopLqi: number = 234;
// Received Zigbee message from '0x', type 'readResponse', cluster 'genBasic', data '{"zclVersion":3}' from endpoint 1 with groupID 0
const messageContents = Buffer.from("1803010000002003", "hex");
mockEzspSend.mockResolvedValueOnce([status, 0]).mockImplementationOnce(() => {
setTimeout(async () => {
mockEzspEmitter.emit(
"incomingMessage",
EmberIncomingMessageType.UNICAST,
reverseApsFrame(apsFrame),
lastHopLqi,
networkAddress,
messageContents,
);
await flushPromises();
}, 300);
return [SLStatus.OK, ++mockAPSSequence];
});
const p = adapter.sendZclFrameToEndpoint("0x1122334455667788", networkAddress, endpoint, zclFrame, 10000, false, false, sourceEndpoint);
await vi.advanceTimersByTimeAsync(10000);
await expect(p).resolves.toStrictEqual({
clusterID: apsFrame.clusterId,
header: Zcl.Header.fromBuffer(messageContents),
address: networkAddress,
data: messageContents,
endpoint: apsFrame.destinationEndpoint,
linkquality: lastHopLqi,
groupID: apsFrame.groupId,
wasBroadcast: false,
destinationEndpoint: apsFrame.sourceEndpoint,
} as ZclPayload);
expect(mockEzspSend).toHaveBeenCalledTimes(2);
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it("Adapter impl: throws when sendZclFrameToEndpoint throws NO_TX_SPACE status and recovery disabled", async () => {
const networkAddress: NodeId = 1234;
const endpoint: number = 1;
const sourceEndpoint = FIXED_ENDPOINTS[0].endpoint;
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: 0,
sequence: 0, // set by stack
};
mockEzspSend.mockRejectedValueOnce(new EzspError(EzspStatus.NO_TX_SPACE));
const p = defuseRejection(
adapter.sendZclFrameToEndpoint(
"0x1122334455667788",
networkAddress,
endpoint,
zclFrame,
10000,
false,
true, // disable recovery
sourceEndpoint,
),
);
await vi.advanceTimersByTimeAsync(10000);
await expect(p).rejects.toThrow(
`~x~> [ZCL to=0x1122334455667788:1234 apsFrame={"profileId":260,"clusterId":0,"sourceEndpoint":1,"destinationEndpoint":1,"options":4416,"groupId":0,"sequence":0}] Failed to send request with status=${SLStatus[SLStatus.BUSY]}.`,
);
expect(mockEzspSend).toHaveBeenCalledTimes(1);
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it("Adapter impl: throws when sendZclFrameToEndpoint get BUSY status from NCP and recovery disabled", async () => {
const networkAddress: NodeId = 1234;
const endpoint: number = 1;
const sourceEndpoint = FIXED_ENDPOINTS[0].endpoint;
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: 0,
sequence: 0, // set by stack
};
mockEzspSend.mockResolvedValueOnce([SLStatus.BUSY, 0]);
const p = defuseRejection(
adapter.sendZclFrameToEndpoint(
"0x1122334455667788",
networkAddress,
endpoint,
zclFrame,
10000,
false,
true, // disable recovery
sourceEndpoint,
),
);
await vi.advanceTimersByTimeAsync(10000);
await expect(p).rejects.toThrow(
`~x~> [ZCL to=0x1122334455667788:1234 apsFrame={"profileId":260,"clusterId":0,"sourceEndpoint":1,"destinationEndpoint":1,"options":4416,"groupId":0,"sequence":0}] Failed to send request with status=${SLStatus[SLStatus.BUSY]}.`,
);
expect(mockEzspSend).toHaveBeenCalledTimes(1);
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it("Adapter impl: throws when sendZclFrameToEndpoint get BUSY status from NCP and exceeded max attempts", async () => {
const networkAddress: NodeId = 1234;
const endpoint: number = 1;
const sourceEndpoint = FIXED_ENDPOINTS[0].endpoint;
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: 0,
sequence: 0, // set by stack
};
mockEzspSend
.mockResolvedValueOnce([SLStatus.BUSY, 0])
.mockResolvedValueOnce([SLStatus.BUSY, 0])
.mockResolvedValueOnce([SLStatus.BUSY, 0]);
const p = defuseRejection(
adapter.sendZclFrameToEndpoint(
"0x1122334455667788",
networkAddress,
endpoint,
zclFrame,
10000,
false,
false, // disable recovery
sourceEndpoint,
),
);
await vi.advanceTimersByTimeAsync(10000);
await expect(p).rejects.toThrow(
`~x~> [ZCL to=0x1122334455667788:1234 apsFrame={"profileId":260,"clusterId":0,"sourceEndpoint":1,"destinationEndpoint":1,"options":4416,"groupId":0,"sequence":0}] Failed to send request with status=${SLStatus[SLStatus.BUSY]}.`,
);
expect(mockEzspSend).toHaveBeenCalledTimes(3);
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it("Adapter impl: throws when sendZclFrameToEndpoint request fails", async () => {
const networkAddress: NodeId = 1234;
const endpoint: number = 1;
const sourceEndpoint = FIXED_ENDPOINTS[0].endpoint;
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: 0,
sequence: 0, // set by stack
};
mockEzspSend.mockResolvedValueOnce([SLStatus.FAIL, 0]);
const p = defuseRejection(
adapter.sendZclFrameToEndpoint(
"0x1122334455667788",
networkAddress,
endpoint,
zclFrame,
10000,
false,
false, // disable recovery
sourceEndpoint,
),
);
await vi.advanceTimersByTimeAsync(10000);
await expect(p).rejects.toThrow(
`~x~> [ZCL to=0x1122334455667788:1234 apsFrame={"profileId":260,"clusterId":0,"sourceEndpoint":1,"destinationEndpoint":1,"options":4416,"groupId":0,"sequence":0}] Failed to send request with status=FAIL.`,
);
expect(mockEzspSend).toHaveBeenCalledTimes(1);
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it("Adapter impl: sendZdo with EUI64", async () => {
const sender: NodeId = 0x6789;
const senderEUI64: Eui64 = "0x1122334455667788";
const apsFrame: EmberApsFrame = {
profileId: Zdo.ZDO_PROFILE_ID,
clusterId: Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE,
sourceEndpoint: Zdo.ZDO_ENDPOINT,
destinationEndpoint: Zdo.ZDO_ENDPOINT,
options: 0,
groupId: 0,
sequence: 0,
};
mockEzspSendBroadcast.mockImplementationOnce(() => {
setTimeout(async () => {
mockEzspEmitter.emit(
"zdoResponse",
apsFrame,
sender,
Buffer.from([
1,
Zdo.Status.SUCCESS,
0x88,
0x77,
0x66,
0x55,
0x44,
0x33,
0x22,
0x11,
0x89, // nwkAddress
0x67, // nwkAddress
]),
);
await flushPromises();
}, 300);
return [SLStatus.OK, ++mockAPSSequence];
});
const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, senderEUI64, false, 0);
const p = adapter.sendZdo(
senderEUI64,
ZSpec.NULL_NODE_ID /* same as broadcast SLEEPY */,
Zdo.ClusterId.NETWORK_ADDRESS_REQUEST,
zdoPayload,
false,
);
await vi.advanceTimersByTimeAsync(1000);
await expect(p).resolves.toStrictEqual([
Zdo.Status.SUCCESS,
{
eui64: senderEUI64,
nwkAddress: sender,
startIndex: 0,
assocDevList: [],
} as ZdoTypes.NetworkAddressResponse,
]);
});
it("Adapter impl: sendZclFrameToEndpoint with default response", async () => {
const networkAddress: NodeId = 1234;
const endpoint: number = 3;
const sourceEndpoint = FIXED_ENDPOINTS[0].endpoint;
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
false,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: 0,
sequence: 0, // set by stack
};
const lastHopLqi: number = 234;
// defaultRsp with cmdId=0, status=0
const messageContents = Buffer.from("18030b0000", "hex");
mockEzspSend.mockImplementationOnce(() => {
setTimeout(async () => {
mockEzspEmitter.emit(
"incomingMessage",
EmberIncomingMessageType.UNICAST,
reverseApsFrame(apsFrame),
lastHopLqi,
networkAddress,
messageContents,
);
await flushPromises();
}, 300);
return [SLStatus.OK, ++mockAPSSequence];
});
const p = adapter.sendZclFrameToEndpoint("0x1122334455667788", networkAddress, endpoint, zclFrame, 10000, true, false, sourceEndpoint);
await vi.advanceTimersByTimeAsync(5000);
await expect(p).resolves.toStrictEqual({
clusterID: apsFrame.clusterId,
header: Zcl.Header.fromBuffer(messageContents),
address: networkAddress,
data: messageContents,
endpoint: apsFrame.destinationEndpoint,
linkquality: lastHopLqi,
groupID: apsFrame.groupId,
wasBroadcast: false,
destinationEndpoint: apsFrame.sourceEndpoint,
} as ZclPayload);
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it("Adapter impl: sendZclFrameToEndpoint without response", async () => {
const networkAddress: NodeId = 1234;
const endpoint: number = 3;
const sourceEndpoint = FIXED_ENDPOINTS[0].endpoint;
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
const p = adapter.sendZclFrameToEndpoint("0x1122334455667788", networkAddress, endpoint, zclFrame, 10000, true, false, sourceEndpoint);
await vi.advanceTimersByTimeAsync(5000);
await expect(p).resolves.toStrictEqual(undefined);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS & ~EmberApsOption.RETRY,
groupId: 0,
sequence: 0, // set by stack
};
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it("Adapter impl: sendZclFrameToGroup with source endpoint", async () => {
const groupId: number = 32;
const zclFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, undefined, 1, 1, 0, [{}], {});
const p = adapter.sendZclFrameToGroup(groupId, zclFrame, 2);
await vi.advanceTimersByTimeAsync(5000);
await expect(p).resolves.toStrictEqual(undefined);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint: 2,
destinationEndpoint: 0xff,
options: DEFAULT_APS_OPTIONS,
groupId,
sequence: 0, // set by stack
};
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.MULTICAST, groupId, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it("Adapter impl: sendZclFrameToGroup with default source endpoint", async () => {
const groupId: number = 32;
const zclFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, undefined, 1, 1, 0, [{}], {});
const p = adapter.sendZclFrameToGroup(groupId, zclFrame);
await vi.advanceTimersByTimeAsync(5000);
await expect(p).resolves.toStrictEqual(undefined);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint: FIXED_ENDPOINTS[0].endpoint,
destinationEndpoint: 0xff,
options: DEFAULT_APS_OPTIONS,
groupId,
sequence: 0, // set by stack
};
expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.MULTICAST, groupId, apsFrame, zclFrame.toBuffer(), 0, 0);
});
it("Adapter impl: throws when sendZclFrameToGroup fails request", async () => {
mockEzspSend.mockResolvedValueOnce([SLStatus.FAIL, 0]);
const groupId: number = 32;
const zclFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, undefined, 1, 1, 0, [{}], {});
const p = defuseRejection(adapter.sendZclFrameToGroup(groupId, zclFrame, 1));
await vi.advanceTimersByTimeAsync(5000);
await expect(p).rejects.toThrow("~x~> [ZCL GROUP groupId=32] Failed to send with status=FAIL.");
expect(mockEzspSend).toHaveBeenCalledTimes(1);
});
it("Adapter impl: sendZclFrameToAll with fixed endpoint", async () => {
const endpoint: number = 32;
const zclFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, undefined, 1, 1, 0, [{}], {});
const sourceEndpoint = FIXED_ENDPOINTS[0].endpoint;
const p = adapter.sendZclFrameToAll(endpoint, zclFrame, sourceEndpoint, ZSpec.BroadcastAddress.DEFAULT);
await vi.advanceTimersByTimeAsync(5000);
await expect(p).resolves.toStrictEqual(undefined);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: ZSpec.BroadcastAddress.DEFAULT,
sequence: 0, // set by stack
};
expect(mockEzspSend).toHaveBeenCalledWith(
EmberOutgoingMessageType.BROADCAST,
ZSpec.BroadcastAddress.DEFAULT,
apsFrame,
zclFrame.toBuffer(),
0,
0,
);
});
it("Adapter impl: sendZclFrameToAll with other endpoint", async () => {
const endpoint: number = 32;
const zclFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, undefined, 1, 1, 0, [{}], {});
const sourceEndpoint = 3;
const p = adapter.sendZclFrameToAll(endpoint, zclFrame, sourceEndpoint, ZSpec.BroadcastAddress.DEFAULT);
await vi.advanceTimersByTimeAsync(5000);
await expect(p).resolves.toStrictEqual(undefined);
const apsFrame: EmberApsFrame = {
profileId: FIXED_ENDPOINTS[0].profileId,
clusterId: zclFrame.cluster.ID,
sourceEndpoint,
destinationEndpoint: endpoint,
options: DEFAULT_APS_OPTIONS,
groupId: ZSpec.BroadcastAddress.DEFAULT,
sequence: 0, // set by stack
};
expect(mockEzspSend).toHaveBeenCalledWith(
EmberOutgoingMessageType.BROADCAST,
ZSpec.BroadcastAddress.DEFAULT,
apsFrame,
zclFrame.toBuffer(),
0,
0,
);
});
it("Adapter impl: throws when sendZclFrameToAll fails request", async () => {
mockEzspSend.mockResolvedValueOnce([SLStatus.FAIL, 0]);
const endpoint: number = 32;
const zclFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, undefined, 1, 1, 0, [{}], {});
const p = defuseRejection(adapter.sendZclFrameToAll(endpoint, zclFrame, 1, ZSpec.BroadcastAddress.DEFAULT));
await vi.advanceTimersByTimeAsync(5000);
await expect(p).rejects.toThrow("~x~> [ZCL BROADCAST destination=65532] Failed to send with status=FAIL.");
expect(mockEzspSend).toHaveBeenCalledTimes(1);
});
it("Adapter impl: setChannelInterPAN", async () => {
await expect(adapter.setChannelInterPAN(15)).resolves.toStrictEqual(undefined);
expect(mockEzspSetLogicalAndRadioChannel).toHaveBeenCalledWith(15);
});
it("Adapter impl: throws when setChannelInterPAN fails request", async () => {
mockEzspSetLogicalAndRadioChannel.mockResolvedValueOnce(SLStatus.FAIL);
await expect(adapter.setChannelInterPAN(15)).rejects.toThrow(`Failed to set InterPAN channel to '15' with status=FAIL.`);
expect(mockEzspSetLogicalAndRadioChannel).toHaveBeenCalledWith(15);
});
it("Adapter impl: sendZclFrameInterPANToIeeeAddr", async () => {
const ieee: Eui64 = "0x1122334455667788";
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
false,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
await expect(adapter.sendZclFrameInterPANToIeeeAddr(zclFrame, ieee)).resolves.toStrictEqual(undefined);
expect(mockEzspSendRawMessage).toHaveBeenCalledTimes(1);
expect(mockEzspSendRawMessage).toHaveBeenCalledWith(expect.any(Buffer), 1, true);
});
it("Adapter impl: throws when sendZclFrameInterPANToIeeeAddr request fails", async () => {
mockEzspSendRawMessage.mockResolvedValueOnce(SLStatus.BUSY);
const ieee: Eui64 = "0x1122334455667788";
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
false,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
await expect(adapter.sendZclFrameInterPANToIeeeAddr(zclFrame, ieee)).rejects.toThrow(
`~x~> [ZCL TOUCHLINK to=${ieee}] Failed to send with status=${SLStatus[SLStatus.BUSY]}.`,
);
expect(mockEzspSendRawMessage).toHaveBeenCalledTimes(1);
expect(mockEzspSendRawMessage).toHaveBeenCalledWith(expect.any(Buffer), 1, true);
});
it("Adapter impl: sendZclFrameInterPANBroadcast", async () => {
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.SPECIFIC,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
0,
"scanRequest",
Zcl.Clusters.touchlink.ID,
{transactionID: 1, zigbeeInformation: 4, touchlinkInformation: 18},
{},
);
const sourcePanId: PanId = 0x1234;
const sourceAddress: Eui64 = "0x1122334455aabbcc";
const groupId: number = ZSpec.BroadcastAddress.SLEEPY;
const lastHopLqi = 252;
// Received Zigbee message from '0x', type 'readResponse', cluster 'genBasic', data '{"zclVersion":3}' from endpoint 1 with groupID 0
const messageContents = Buffer.from("1800010000000100000000000000000088776655443322110154466341200", "hex");
mockEzspSendRawMessage.mockImplementationOnce(() => {
setTimeout(async () => {
mockEzspEmitter.emit("touchlinkMessage", sourcePanId, sourceAddress, groupId, lastHopLqi, messageContents);
await flushPromises();
}, 300);
return SLStatus.OK;
});
const p = adapter.sendZclFrameInterPANBroadcast(zclFrame, 10000);
await vi.advanceTimersByTimeAsync(5000);
const payload: ZclPayload = {
clusterID: Zcl.Clusters.touchlink.ID,
header: Zcl.Header.fromBuffer(messageContents),
address: sourceAddress,
data: messageContents,
endpoint: FIXED_ENDPOINTS[0].endpoint,
linkquality: lastHopLqi,
groupID: groupId,
wasBroadcast: true,
destinationEndpoint: FIXED_ENDPOINTS[0].endpoint,
};
await expect(p).resolves.toStrictEqual(payload);
expect(mockEzspSendRawMessage).toHaveBeenCalledTimes(1);
expect(mockEzspSendRawMessage).toHaveBeenCalledWith(expect.any(Buffer), 1, true);
});
it("Adapter impl: throws when sendZclFrameInterPANBroadcast command has no response", async () => {
const commandName = "readRsp";
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
false,
undefined,
3,
commandName,
"genBasic",
[{attrId: 0}],
{},
);
await expect(adapter.sendZclFrameInterPANBroadcast(zclFrame, 10000)).rejects.toThrow(
`Command '${commandName}' has no response, cannot wait for response.`,
);
expect(mockEzspSendRawMessage).toHaveBeenCalledTimes(0);
});
it("Adapter impl: throws when sendZclFrameInterPANBroadcast request fails", async () => {
mockEzspSendRawMessage.mockResolvedValueOnce(SLStatus.BUSY);
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.GLOBAL,
Zcl.Direction.CLIENT_TO_SERVER,
false,
undefined,
3,
"read",
"genBasic",
[{attrId: 0}],
{},
);
await expect(adapter.sendZclFrameInterPANBroadcast(zclFrame, 10000)).rejects.toThrow(
`~x~> [ZCL TOUCHLINK BROADCAST] Failed to send with status=${SLStatus[SLStatus.BUSY]}.`,
);
expect(mockEzspSendRawMessage).toHaveBeenCalledTimes(1);
expect(mockEzspSendRawMessage).toHaveBeenCalledWith(expect.any(Buffer), 1, true);
});
it("Adapter impl: restoreChannelInterPAN", async () => {
const p = adapter.restoreChannelInterPAN();
await vi.advanceTimersByTimeAsync(10000);
await expect(p).resolves.toStrictEqual(undefined);
expect(mockEzspSetLogicalAndRadioChannel).toHaveBeenCalledWith(DEFAULT_NETWORK_OPTIONS.channelList[0]);
});
it("Adapter impl: throws when restoreChannelInterPAN fails request", async () => {
mockEzspSetLogicalAndRadioChannel.mockResolvedValueOnce(SLStatus.FAIL);
const p = defuseRejection(adapter.restoreChannelInterPAN());
await vi.advanceTimersByTimeAsync(10000);
await expect(p).rejects.toThrow(`Failed to restore InterPAN channel to '${DEFAULT_NETWORK_OPTIONS.channelList[0]}' with status=FAIL.`);
expect(mockEzspSetLogicalAndRadioChannel).toHaveBeenCalledWith(DEFAULT_NETWORK_OPTIONS.channelList[0]);
});
});
});