From cdf8a64d27e03c063fca1489b92f2407ddcaa62d Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Wed, 11 Dec 2024 19:39:53 +0100 Subject: [PATCH 01/16] feat: Generic start strategy --- src/adapter/adapter.ts | 142 ++++++++- src/adapter/ember/adapter/emberAdapter.ts | 352 ++++++++-------------- src/adapter/tstype.ts | 6 +- test/adapter/ember/emberAdapter.test.ts | 264 +++++++--------- 4 files changed, 381 insertions(+), 383 deletions(-) diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index 255344499d..e97c50e899 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -1,6 +1,9 @@ -import events from 'events'; +import assert from 'node:assert'; +import events from 'node:events'; +import {accessSync, readFileSync} from 'node:fs'; import * as Models from '../models'; +import {BackupUtils} from '../utils'; import {BroadcastAddress} from '../zspec/enums'; import * as Zcl from '../zspec/zcl'; import * as Zdo from '../zspec/zdo'; @@ -77,10 +80,108 @@ abstract class Adapter extends events.EventEmitter { } } + /** + * Get the strategy to use during start. + * - resumed: network in configuration.yaml matches network in adapter, resume operation + * - reset: network in configuration.yaml does not match network in adapter, no valid backup, form a new network + * - restored: network in configuration.yaml does not match network in adapter, valid backup, restore from backup + */ + public async initNetwork(): Promise { + const enum InitAction { + DONE, + /** Config mismatch, must leave network. */ + LEAVE, + /** Config mismatched, left network. Will evaluate forming from backup or config next. */ + LEFT, + /** Form the network using config. No backup, or backup mismatch. */ + FORM_CONFIG, + /** Re-form the network using full backed-up data. */ + FORM_BACKUP, + } + + const [hasNetwork, panID, extendedPanID] = await this.hasNetwork(); + const extendedPanIDBuffer = Buffer.from(this.networkOptions.extendedPanID); + const configNetworkKeyBuffer = Buffer.from(this.networkOptions.networkKey!); + let action: InitAction = InitAction.DONE; + + if (hasNetwork) { + // has a network + if (this.networkOptions.panID === panID && extendedPanIDBuffer.equals(extendedPanID)) { + // pan matches + if (!configNetworkKeyBuffer.equals(await this.getNetworkKey())) { + // network key does not match + action = InitAction.LEAVE; + } + } else { + // pan does not match + action = InitAction.LEAVE; + } + + if (action === InitAction.LEAVE) { + // mismatch, force network leave + await this.leaveNetwork(); + + action = InitAction.LEFT; + } + } + + const backup = this.getStoredBackup(); + + if (!hasNetwork || action === InitAction.LEFT) { + // no network + if (backup !== undefined) { + // valid backup + if ( + this.networkOptions.panID === backup.networkOptions.panId && + extendedPanIDBuffer.equals(backup.networkOptions.extendedPanId) && + this.networkOptions.channelList.includes(backup.logicalChannel) && + configNetworkKeyBuffer.equals(backup.networkOptions.networkKey) + ) { + // config matches backup + action = InitAction.FORM_BACKUP; + } else { + // TODO: this should be changed to write config instead, and FORM_BACKUP (i.e. support loading backup from scratch) + // config does not match backup + action = InitAction.FORM_CONFIG; + } + } else { + // no backup + action = InitAction.FORM_CONFIG; + } + } + + //---- from here on, we assume everything is in place for whatever decision was taken above + + switch (action) { + case InitAction.FORM_BACKUP: { + assert(backup); + await this.formNetwork(backup); + + return 'restored'; + } + case InitAction.FORM_CONFIG: { + await this.formNetwork(); + + return 'reset'; + } + case InitAction.DONE: { + return 'resumed'; + } + } + } + public abstract start(): Promise; public abstract stop(): Promise; + public abstract hasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]>; + + public abstract leaveNetwork(): Promise; + + public abstract formNetwork(backup?: Models.Backup): Promise; + + public abstract getNetworkKey(): Promise; + public abstract getCoordinatorIEEE(): Promise; public abstract getCoordinatorVersion(): Promise; @@ -89,6 +190,45 @@ abstract class Adapter extends events.EventEmitter { public abstract supportsBackup(): Promise; + /** + * Loads currently stored backup and returns it in internal backup model. + */ + public getStoredBackup(): Models.Backup | undefined { + try { + accessSync(this.backupPath); + } catch { + return undefined; + } + + try { + const data = JSON.parse(readFileSync(this.backupPath, 'utf8')) as Models.UnifiedBackupStorage | Models.LegacyBackupStorage; + + if ('adapterType' in data) { + const backup = BackupUtils.fromLegacyBackup(data as Models.LegacyBackupStorage); + + this.checkBackup(backup); + + return backup; + } else if (data.metadata?.format === 'zigpy/open-coordinator-backup' && data.metadata?.version) { + if (data.metadata?.version !== 1) { + throw new Error(`Unsupported open coordinator backup version (version=${data.metadata?.version})`); + } + + const backup = BackupUtils.fromUnifiedBackup(data as Models.UnifiedBackupStorage); + + this.checkBackup(backup); + + return backup; + } + } catch (error) { + throw new Error(`Coordinator backup is corrupted (${error})`); + } + + throw new Error('Unknown backup format'); + } + + public abstract checkBackup(backup: Models.Backup): void; + public abstract backup(ieeeAddressesInDatabase: string[]): Promise; public abstract getNetworkParameters(): Promise; diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 4d097cf2cb..a7745944df 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -1,12 +1,12 @@ import {randomBytes} from 'crypto'; -import {existsSync, readFileSync, renameSync} from 'fs'; +import {readFileSync} from 'fs'; import path from 'path'; import equals from 'fast-deep-equal/es6'; import {Adapter, TsType} from '../..'; -import {Backup, UnifiedBackupStorage} from '../../../models'; -import {BackupUtils, Queue, Wait} from '../../../utils'; +import {Backup} from '../../../models'; +import {Queue, Wait} from '../../../utils'; import {logger} from '../../../utils/logger'; import * as ZSpec from '../../../zspec'; import {EUI64, ExtendedPanId, NodeId, PanId} from '../../../zspec/tstypes'; @@ -94,19 +94,6 @@ export type LinkKeyBackupData = { incomingFrameCounter: number; }; -enum NetworkInitAction { - /** Ain't that nice! */ - DONE, - /** Config mismatch, must leave network. */ - LEAVE, - /** Config mismatched, left network. Will evaluate forming from backup or config next. */ - LEFT, - /** Form the network using config. No backup, or backup mismatch. */ - FORM_CONFIG, - /** Re-form the network using full backed-up data. */ - FORM_BACKUP, -} - /** * Application generated ZDO messages use sequence numbers 0-127, and the stack * uses sequence numbers 128-255. This simplifies life by eliminating the need @@ -116,8 +103,6 @@ enum NetworkInitAction { const APPLICATION_ZDO_SEQUENCE_MASK = 0x7f; /* Default radius used for broadcast ZDO requests. uint8_t */ const ZDO_REQUEST_RADIUS = 0xff; -/** Oldest supported EZSP version for backups. Don't take the risk to restore a broken network until older backup versions can be investigated. */ -const BACKUP_OLDEST_SUPPORTED_EZSP_VERSION = 12; /** * 9sec is minimum recommended for `ezspBroadcastNextNetworkKey` to have propagated throughout network. * NOTE: This is blocking the request queue, so we shouldn't go crazy high. @@ -856,184 +841,13 @@ export class EmberAdapter extends Adapter { } } - const configNetworkKey = Buffer.from(this.networkOptions.networkKey!); - const networkInitStruct: EmberNetworkInitStruct = { - bitmask: EmberNetworkInitBitmask.PARENT_INFO_IN_TOKEN | EmberNetworkInitBitmask.END_DEVICE_REJOIN_ON_REBOOT, - }; - const initStatus = await this.ezsp.ezspNetworkInit(networkInitStruct); - - logger.debug(`[INIT TC] Network init status=${SLStatus[initStatus]}.`, NS); - - if (initStatus !== SLStatus.OK && initStatus !== SLStatus.NOT_JOINED) { - throw new Error(`[INIT TC] Failed network init request with status=${SLStatus[initStatus]}.`); - } - - let action: NetworkInitAction = NetworkInitAction.DONE; - - if (initStatus === SLStatus.OK) { - // network - await this.oneWaitress.startWaitingForEvent( - {eventName: OneWaitressEvents.STACK_STATUS_NETWORK_UP}, - DEFAULT_NETWORK_REQUEST_TIMEOUT, - '[INIT TC] Network init', - ); - - const [npStatus, nodeType, netParams] = await this.ezsp.ezspGetNetworkParameters(); - - logger.debug(() => `[INIT TC] Current adapter network: nodeType=${EmberNodeType[nodeType]} params=${JSON.stringify(netParams)}`, NS); - - if ( - npStatus === SLStatus.OK && - nodeType === EmberNodeType.COORDINATOR && - this.networkOptions.panID === netParams.panId && - equals(this.networkOptions.extendedPanID, netParams.extendedPanId) - ) { - // config matches adapter so far, no error, we can check the network key - const context = initSecurityManagerContext(); - context.coreKeyType = SecManKeyType.NETWORK; - context.keyIndex = 0; - const [nkStatus, networkKey] = await this.ezsp.ezspExportKey(context); - - if (nkStatus !== SLStatus.OK) { - throw new Error(`[INIT TC] Failed to export Network Key with status=${SLStatus[nkStatus]}.`); - } - - // config doesn't match adapter anymore - if (!networkKey.contents.equals(configNetworkKey)) { - action = NetworkInitAction.LEAVE; - } - } else { - // config doesn't match adapter - action = NetworkInitAction.LEAVE; - } - - if (action === NetworkInitAction.LEAVE) { - logger.info(`[INIT TC] Adapter network does not match config. Leaving network...`, NS); - const leaveStatus = await this.ezsp.ezspLeaveNetwork(); - - if (leaveStatus !== SLStatus.OK) { - throw new Error(`[INIT TC] Failed leave network request with status=${SLStatus[leaveStatus]}.`); - } - - await this.oneWaitress.startWaitingForEvent( - {eventName: OneWaitressEvents.STACK_STATUS_NETWORK_DOWN}, - DEFAULT_NETWORK_REQUEST_TIMEOUT, - '[INIT TC] Leave network', - ); - - await Wait(200); // settle down - - action = NetworkInitAction.LEFT; - } - } - - const backup = this.getStoredBackup(); - - if (initStatus === SLStatus.NOT_JOINED || action === NetworkInitAction.LEFT) { - // no network - if (backup != undefined) { - if ( - this.networkOptions.panID === backup.networkOptions.panId && - Buffer.from(this.networkOptions.extendedPanID!).equals(backup.networkOptions.extendedPanId) && - this.networkOptions.channelList.includes(backup.logicalChannel) && - configNetworkKey.equals(backup.networkOptions.networkKey) - ) { - // config matches backup - action = NetworkInitAction.FORM_BACKUP; - } else { - // config doesn't match backup - logger.info(`[INIT TC] Config does not match backup.`, NS); - action = NetworkInitAction.FORM_CONFIG; - } - } else { - // no backup - logger.info(`[INIT TC] No valid backup found.`, NS); - action = NetworkInitAction.FORM_CONFIG; - } - } - - //---- from here on, we assume everything is in place for whatever decision was taken above - - let result: TsType.StartResult = 'resumed'; - - switch (action) { - case NetworkInitAction.FORM_BACKUP: { - logger.info(`[INIT TC] Forming from backup.`, NS); - // `backup` valid in this `action` path (not detected by TS) - /* istanbul ignore next */ - const keyList: LinkKeyBackupData[] = backup!.devices.map((device) => ({ - deviceEui64: ZSpec.Utils.eui64BEBufferToHex(device.ieeeAddress), - key: {contents: device.linkKey!.key}, - outgoingFrameCounter: device.linkKey!.txCounter, - incomingFrameCounter: device.linkKey!.rxCounter, - })); - - // before forming - await this.importLinkKeys(keyList); - - await this.formNetwork( - true /*from backup*/, - backup!.networkOptions.networkKey, - backup!.networkKeyInfo.sequenceNumber, - backup!.networkKeyInfo.frameCounter, - backup!.networkOptions.panId, - Array.from(backup!.networkOptions.extendedPanId), - backup!.logicalChannel, - backup!.ezsp!.hashed_tclk!, // valid from getStoredBackup - ); - - result = 'restored'; - break; - } - case NetworkInitAction.FORM_CONFIG: { - logger.info(`[INIT TC] Forming from config.`, NS); - await this.formNetwork( - false /*from config*/, - configNetworkKey, - 0, - 0, - this.networkOptions.panID, - this.networkOptions.extendedPanID!, - this.networkOptions.channelList[0], - randomBytes(EMBER_ENCRYPTION_KEY_SIZE), // rnd TC link key - ); - - result = 'reset'; - break; - } - case NetworkInitAction.DONE: { - logger.info(`[INIT TC] Adapter network matches config.`, NS); - break; - } - } - - // can't let frame counter wrap to zero (uint32_t), will force a broadcast after init if getting too close - if (backup != null && backup.networkKeyInfo.frameCounter > 0xfeeeeeee) { - // XXX: while this remains a pretty low occurrence in most (small) networks, - // currently Z2M won't support the key update because of one-way config... - // need to investigate handling this properly - - // logger.warning(`[INIT TC] Network key frame counter is reaching its limit. Scheduling broadcast to update network key. ` - // + `This may result in some devices (especially battery-powered) temporarily losing connection.`, NS); - // // XXX: no idea here on the proper timer value, but this will block the network for several seconds on exec - // // (probably have to take the behavior of sleepy-end devices into account to improve chances of reaching everyone right away?) - // setTimeout(async () => { - // this.requestQueue.enqueue(async (): Promise => { - // await this.broadcastNetworkKeyUpdate(); - - // return SLStatus.OK; - // }, logger.error, true);// no reject just log error if any, will retry next start, & prioritize so we know it'll run when expected - // }, 300000); - logger.warning(`[INIT TC] Network key frame counter is reaching its limit. A new network key will have to be instaured soon.`, NS); - } - - return result; + return await this.initNetwork(); } /** * Form a network using given parameters. */ - private async formNetwork( + private async formNetworkInternal( fromBackup: boolean, networkKey: Buffer, networkKeySequenceNumber: number, @@ -1125,43 +939,6 @@ export class EmberAdapter extends Adapter { logger.info(`[INIT FORM] New network formed!`, NS); } - /** - * Loads currently stored backup and returns it in internal backup model. - */ - private getStoredBackup(): Backup | undefined { - if (!existsSync(this.backupPath)) { - return undefined; - } - - let data: UnifiedBackupStorage; - - try { - data = JSON.parse(readFileSync(this.backupPath).toString()); - } catch (error) { - throw new Error(`[BACKUP] Coordinator backup is corrupted. (${(error as Error).stack})`); - } - - if (data.metadata?.format === 'zigpy/open-coordinator-backup' && data.metadata?.version) { - if (data.metadata?.version !== 1) { - throw new Error(`[BACKUP] Unsupported open coordinator backup version (version=${data.metadata?.version}).`); - } - - if (!data.stack_specific?.ezsp || !data.metadata.internal.ezspVersion) { - throw new Error(`[BACKUP] Current backup file is not for EmberZNet stack.`); - } - - if (data.metadata.internal.ezspVersion < BACKUP_OLDEST_SUPPORTED_EZSP_VERSION) { - renameSync(this.backupPath, `${this.backupPath}.old`); - logger.warning(`[BACKUP] Current backup file is from an unsupported EZSP version. Renaming and ignoring.`, NS); - return undefined; - } - - return BackupUtils.fromUnifiedBackup(data); - } else { - throw new Error(`[BACKUP] Unknown backup format.`); - } - } - /** * Export link keys for backup. * @@ -1550,6 +1327,119 @@ export class EmberAdapter extends Adapter { logger.info(`======== Ember Adapter Stopped ========`, NS); } + public async hasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { + // when called after init TC, should always return true + if (this.networkCache.parameters.panId !== ZSpec.INVALID_PAN_ID) { + return [true, this.networkCache.parameters.panId, Buffer.from(this.networkCache.parameters.extendedPanId)]; + } else { + const networkInitStruct: EmberNetworkInitStruct = { + bitmask: EmberNetworkInitBitmask.PARENT_INFO_IN_TOKEN | EmberNetworkInitBitmask.END_DEVICE_REJOIN_ON_REBOOT, + }; + const initStatus = await this.ezsp.ezspNetworkInit(networkInitStruct); + + logger.debug(`Network init status=${SLStatus[initStatus]}.`, NS); + + if (initStatus !== SLStatus.OK && initStatus !== SLStatus.NOT_JOINED) { + throw new Error(`Failed network init request with status=${SLStatus[initStatus]}.`); + } + + if (initStatus === SLStatus.OK) { + await this.oneWaitress.startWaitingForEvent( + {eventName: OneWaitressEvents.STACK_STATUS_NETWORK_UP}, + DEFAULT_NETWORK_REQUEST_TIMEOUT, + 'Network init', + ); + + const [npStatus, nodeType, netParams] = await this.ezsp.ezspGetNetworkParameters(); + + if (npStatus !== SLStatus.OK) { + throw new Error(`Failed to get network parameters with status=${SLStatus[npStatus]}.`); + } + + logger.debug(() => `Current adapter network: nodeType=${EmberNodeType[nodeType]} params=${JSON.stringify(netParams)}`, NS); + + // force forming in case current network is `ROUTER` type + if (nodeType !== EmberNodeType.COORDINATOR) { + return [false, undefined, undefined]; + } + + return [true, netParams.panId, Buffer.from(netParams.extendedPanId)]; + } + + return [false, undefined, undefined]; + } + } + + public async leaveNetwork(): Promise { + const leaveStatus = await this.ezsp.ezspLeaveNetwork(); + + if (leaveStatus !== SLStatus.OK) { + throw new Error(`Failed leave network request with status=${SLStatus[leaveStatus]}.`); + } + + await this.oneWaitress.startWaitingForEvent( + {eventName: OneWaitressEvents.STACK_STATUS_NETWORK_DOWN}, + DEFAULT_NETWORK_REQUEST_TIMEOUT, + 'Leave network', + ); + + await Wait(200); // settle down + } + + public async formNetwork(backup?: Backup): Promise { + if (backup) { + logger.info(`[INIT FORM] Forming from backup.`, NS); + // `backup` valid in this `action` path (not detected by TS) + /* istanbul ignore next */ + const keyList: LinkKeyBackupData[] = backup!.devices.map((device) => ({ + deviceEui64: ZSpec.Utils.eui64BEBufferToHex(device.ieeeAddress), + key: {contents: device.linkKey!.key}, + outgoingFrameCounter: device.linkKey!.txCounter, + incomingFrameCounter: device.linkKey!.rxCounter, + })); + + // before forming + await this.importLinkKeys(keyList); + + await this.formNetworkInternal( + true /*from backup*/, + backup.networkOptions.networkKey, + backup.networkKeyInfo.sequenceNumber, + backup.networkKeyInfo.frameCounter, + backup.networkOptions.panId, + Array.from(backup.networkOptions.extendedPanId), + backup.logicalChannel, + backup.ezsp!.hashed_tclk!, // valid from checkBackup + ); + } else { + logger.info(`[INIT FORM] Forming from config.`, NS); + await this.formNetworkInternal( + false /*from config*/, + Buffer.from(this.networkOptions.networkKey), + 0, + 0, + this.networkOptions.panID, + this.networkOptions.extendedPanID, + this.networkOptions.channelList[0], + randomBytes(EMBER_ENCRYPTION_KEY_SIZE), // rnd TC link key + ); + } + } + + public async getNetworkKey(): Promise { + // config matches adapter so far, no error, we can check the network key + const context = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.NETWORK; + context.keyIndex = 0; + const [nkStatus, networkKey] = await this.ezsp.ezspExportKey(context); + + if (nkStatus !== SLStatus.OK) { + throw new Error(`Failed to export Network Key with status=${SLStatus[nkStatus]}.`); + } + + return networkKey.contents; + } + public async getCoordinatorIEEE(): Promise { return await this.queue.execute(async () => { this.checkInterpanLock(); @@ -1575,6 +1465,12 @@ export class EmberAdapter extends Adapter { return true; } + public checkBackup(backup: Backup): void { + if (!backup.ezsp?.hashed_tclk) { + throw new Error(`[BACKUP] Current backup file is not for EmberZNet stack.`); + } + } + // queued // eslint-disable-next-line @typescript-eslint/no-unused-vars public async backup(ieeeAddressesInDatabase: string[]): Promise { diff --git a/src/adapter/tstype.ts b/src/adapter/tstype.ts index e7402a2460..bfea2244da 100644 --- a/src/adapter/tstype.ts +++ b/src/adapter/tstype.ts @@ -10,10 +10,10 @@ export type USBAdapterFingerprint = { export interface NetworkOptions { panID: number; - extendedPanID?: number[]; + extendedPanID: number[]; channelList: number[]; - networkKey?: number[]; - networkKeyDistribute?: boolean; + networkKey: number[]; + networkKeyDistribute: boolean; } export interface SerialPortOptions { diff --git a/test/adapter/ember/emberAdapter.test.ts b/test/adapter/ember/emberAdapter.test.ts index bca411cf2c..2ef46e8ccf 100644 --- a/test/adapter/ember/emberAdapter.test.ts +++ b/test/adapter/ember/emberAdapter.test.ts @@ -131,141 +131,143 @@ const DEFAULT_ADAPTER_NETWORK_PARAMETERS: EmberNetworkParameters = { nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, }; +const DEFAULT_NETWORK_KEY_INFO: SecManNetworkKeyInfo = { + networkKeySet: true, + alternateNetworkKeySet: false, + networkKeySequenceNumber: DEFAULT_BACKUP.network_key.sequence_number, + altNetworkKeySequenceNumber: 0, + networkKeyFrameCounter: DEFAULT_BACKUP.network_key.frame_counter, +}; let mockManufCode = Zcl.ManufacturerCode.SILICON_LABORATORIES; let mockAPSSequence = -1; // start at 0 let mockMessageTag = -1; // start at 0 let mockEzspEmitter = new EventEmitter(); -const mockEzspRemoveAllListeners = jest.fn().mockImplementation((e) => { +const mockEzspRemoveAllListeners = jest.fn((e: unknown) => { mockEzspEmitter.removeAllListeners(e); }); -const mockEzspOn = jest.fn().mockImplementation((e, l) => { +const mockEzspOn = jest.fn((e: any, l: (...args: any) => void) => { mockEzspEmitter.on(e, l); }); -const mockEzspOnce = jest.fn().mockImplementation((e, l) => { +const mockEzspOnce = jest.fn((e: any, l: (...args: any) => void) => { mockEzspEmitter.once(e, l); }); -const mockEzspStart = jest.fn().mockResolvedValue(EzspStatus.SUCCESS); +const mockEzspStart = jest.fn(() => Promise.resolve(EzspStatus.SUCCESS)); const mockEzspStop = jest.fn(); -const mockEzspSend = jest.fn().mockResolvedValue([SLStatus.OK, ++mockMessageTag]); -const mockEzspSetMulticastTableEntry = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspSetManufacturerCode = jest.fn().mockImplementation((code) => (mockManufCode = code)); -const mockEzspReadAndClearCounters = jest.fn().mockResolvedValue([1, 2, 3, 4]); // not matching EmberCounterType, but doesn't matter here -const mockEzspGetNetworkParameters = jest - .fn() - .mockResolvedValue([SLStatus.OK, EmberNodeType.COORDINATOR, deepClone(DEFAULT_ADAPTER_NETWORK_PARAMETERS)]); -const mockEzspNetworkState = jest.fn().mockResolvedValue(EmberNetworkStatus.JOINED_NETWORK); -const mockEzspGetEui64 = jest.fn().mockResolvedValue(DEFAULT_COORDINATOR_IEEE); -const mockEzspSetConcentrator = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspSetSourceRouteDiscoveryMode = jest.fn().mockResolvedValue(1240 /* ms */); -const mockEzspSetRadioIeee802154CcaMode = jest.fn().mockResolvedValue(SLStatus.OK); +const mockEzspSend = jest.fn(() => Promise.resolve([SLStatus.OK, ++mockMessageTag])); +const mockEzspSetMulticastTableEntry = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspSetManufacturerCode = jest.fn((code: Zcl.ManufacturerCode) => (mockManufCode = code)); +const mockEzspReadAndClearCounters = jest.fn(() => Promise.resolve([1, 2, 3, 4])); // not matching EmberCounterType, but doesn't matter here +const mockEzspGetNetworkParameters = jest.fn(() => + Promise.resolve([SLStatus.OK, EmberNodeType.COORDINATOR, deepClone(DEFAULT_ADAPTER_NETWORK_PARAMETERS)]), +); +const mockEzspNetworkState = jest.fn(() => Promise.resolve(EmberNetworkStatus.JOINED_NETWORK)); +const mockEzspGetEui64 = jest.fn(() => Promise.resolve(DEFAULT_COORDINATOR_IEEE)); +const mockEzspSetConcentrator = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspSetSourceRouteDiscoveryMode = jest.fn(() => Promise.resolve(1240 /* ms */)); +const mockEzspSetRadioIeee802154CcaMode = jest.fn(() => Promise.resolve(SLStatus.OK)); // not OK by default since used to detected unreged EP -const mockEzspGetEndpointFlags = jest.fn().mockResolvedValue([SLStatus.NOT_FOUND, EzspEndpointFlag.DISABLED]); -const mockEzspAddEndpoint = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspNetworkInit = jest.fn().mockImplementation((networkInitStruct: EmberNetworkInitStruct) => { +const mockEzspGetEndpointFlags = jest.fn(() => Promise.resolve([SLStatus.NOT_FOUND, EzspEndpointFlag.DISABLED])); +const mockEzspAddEndpoint = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspNetworkInit = jest.fn((networkInitStruct: EmberNetworkInitStruct) => { setTimeout(async () => { mockEzspEmitter.emit('stackStatus', SLStatus.NETWORK_UP); await flushPromises(); }, 300); - return SLStatus.OK; + return Promise.resolve(SLStatus.OK); }); -const mockEzspExportKey = jest.fn().mockImplementation((context: SecManContext) => { +const mockEzspExportKey = jest.fn((context: SecManContext) => { switch (context.coreKeyType) { case SecManKeyType.NETWORK: { - return [SLStatus.OK, {contents: Buffer.from(DEFAULT_BACKUP.network_key.key, 'hex')} as SecManKey]; + return Promise.resolve([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]; + return Promise.resolve([SLStatus.OK, {contents: Buffer.from(DEFAULT_BACKUP.stack_specific!.ezsp!.hashed_tclk!, 'hex')} as SecManKey]); } } }); -const mockEzspLeaveNetwork = jest.fn().mockImplementation(() => { +const mockEzspLeaveNetwork = jest.fn(() => { setTimeout(async () => { mockEzspEmitter.emit('stackStatus', SLStatus.NETWORK_DOWN); await flushPromises(); }, 300); - return SLStatus.OK; + return Promise.resolve(SLStatus.OK); }); -const mockEzspSetInitialSecurityState = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspSetExtendedSecurityBitmask = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspClearKeyTable = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspFormNetwork = jest.fn().mockImplementation((parameters: EmberNetworkParameters) => { +const mockEzspSetInitialSecurityState = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspSetExtendedSecurityBitmask = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspClearKeyTable = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspFormNetwork = jest.fn((parameters: EmberNetworkParameters) => { setTimeout(async () => { mockEzspEmitter.emit('stackStatus', SLStatus.NETWORK_UP); await flushPromises(); }, 300); - return SLStatus.OK; + return Promise.resolve(SLStatus.OK); }); -const mockEzspStartWritingStackTokens = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspGetConfigurationValue = jest.fn().mockImplementation((config: EzspConfigId) => { +const mockEzspStartWritingStackTokens = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspGetConfigurationValue = jest.fn((config: EzspConfigId) => { switch (config) { case EzspConfigId.KEY_TABLE_SIZE: { - return [SLStatus.OK, 0]; + return Promise.resolve([SLStatus.OK, 0]); } } }); const mockEzspExportLinkKeyByIndex = jest.fn(); -const mockEzspEraseKeyTableEntry = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspImportLinkKey = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspBroadcastNextNetworkKey = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspBroadcastNetworkKeySwitch = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspStartScan = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspVersion = jest.fn().mockImplementation((version: number) => [version, EZSP_STACK_TYPE_MESH, 0]); +const mockEzspEraseKeyTableEntry = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspImportLinkKey = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspBroadcastNextNetworkKey = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspBroadcastNetworkKeySwitch = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspStartScan = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspVersion = jest.fn((version: number) => Promise.resolve([version, EZSP_STACK_TYPE_MESH, 0])); const mockEzspSetProtocolVersion = jest.fn(); -const mockEzspGetVersionStruct = jest.fn().mockResolvedValue([ - SLStatus.OK, - { - build: 135, - major: 8, - minor: 0, - patch: 0, - special: 0, - type: EmberVersionType.GA, - } as EmberVersion, -]); -const mockEzspSetConfigurationValue = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspSetValue = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspSetPolicy = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspPermitJoining = jest.fn().mockImplementation((duration: number) => { +const mockEzspGetVersionStruct = jest.fn(() => + Promise.resolve([ + SLStatus.OK, + { + build: 135, + major: 8, + minor: 0, + patch: 0, + special: 0, + type: EmberVersionType.GA, + } as EmberVersion, + ]), +); +const mockEzspSetConfigurationValue = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspSetValue = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspSetPolicy = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspPermitJoining = jest.fn((duration: number) => { setTimeout(async () => { mockEzspEmitter.emit('stackStatus', duration > 0 ? SLStatus.ZIGBEE_NETWORK_OPENED : SLStatus.ZIGBEE_NETWORK_CLOSED); await flushPromises(); }, 300); - return SLStatus.OK; + return Promise.resolve(SLStatus.OK); }); -const mockEzspSendBroadcast = jest.fn().mockResolvedValue([SLStatus.OK, ++mockAPSSequence]); -const mockEzspSendUnicast = jest.fn().mockResolvedValue([SLStatus.OK, ++mockAPSSequence]); -const mockEzspGetNetworkKeyInfo = jest.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 = jest.fn().mockResolvedValue([ - SLStatus.OK, - { - bitmask: EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER, - outgoingFrameCounter: 456, - incomingFrameCounter: 0, - ttlInSeconds: 0, - } as SecManAPSKeyMetadata, -]); -const mockEzspSetRadioPower = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspImportTransientKey = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspClearTransientLinkKeys = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspSetLogicalAndRadioChannel = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspSendRawMessage = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspSetNWKFrameCounter = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspSetAPSFrameCounter = jest.fn().mockResolvedValue(SLStatus.OK); +const mockEzspSendBroadcast = jest.fn(() => Promise.resolve([SLStatus.OK, ++mockAPSSequence])); +const mockEzspSendUnicast = jest.fn(() => Promise.resolve([SLStatus.OK, ++mockAPSSequence])); +const mockEzspGetNetworkKeyInfo = jest.fn(() => Promise.resolve([SLStatus.OK, deepClone(DEFAULT_NETWORK_KEY_INFO)])); +const mockEzspGetApsKeyInfo = jest.fn(() => + Promise.resolve([ + SLStatus.OK, + { + bitmask: EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER, + outgoingFrameCounter: 456, + incomingFrameCounter: 0, + ttlInSeconds: 0, + } as SecManAPSKeyMetadata, + ]), +); +const mockEzspSetRadioPower = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspImportTransientKey = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspClearTransientLinkKeys = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspSetLogicalAndRadioChannel = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspSendRawMessage = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspSetNWKFrameCounter = jest.fn(() => Promise.resolve(SLStatus.OK)); +const mockEzspSetAPSFrameCounter = jest.fn(() => Promise.resolve(SLStatus.OK)); jest.mock('../../../src/adapter/ember/uart/ash'); @@ -919,7 +921,7 @@ describe('Ember Adapter Layer', () => { () => { mockEzspGetNetworkParameters .mockResolvedValueOnce([SLStatus.OK, EmberNodeType.COORDINATOR, deepClone(DEFAULT_ADAPTER_NETWORK_PARAMETERS)]) - .mockResolvedValueOnce([SLStatus.FAIL, 0, {}]); + .mockResolvedValueOnce([SLStatus.FAIL, 0, deepClone(DEFAULT_ADAPTER_NETWORK_PARAMETERS)]); }, `Failed to get network parameters with status=FAIL.`, ], @@ -982,23 +984,22 @@ describe('Ember Adapter Layer', () => { () => { mockEzspNetworkInit.mockResolvedValueOnce(SLStatus.FAIL); }, - `[INIT TC] Failed network init request with status=FAIL.`, + `Failed network init request with status=FAIL.`, ], [ 'if could not export network key', () => { - mockEzspExportKey.mockResolvedValueOnce([SLStatus.FAIL, Buffer.alloc(16)]); + mockEzspExportKey.mockResolvedValueOnce([SLStatus.FAIL, {contents: Buffer.alloc(16)}]); }, - `[INIT TC] Failed to export Network Key with status=FAIL.`, + `Failed to export Network Key with status=FAIL.`, ], [ 'if could not leave network', () => { - // force leave code path - mockEzspGetNetworkParameters.mockResolvedValueOnce([SLStatus.FAIL, 0, {}]); + takeResetCodePath(); mockEzspLeaveNetwork.mockResolvedValueOnce(SLStatus.FAIL); }, - `[INIT TC] Failed leave network request with status=FAIL.`, + `Failed leave network request with status=FAIL.`, ], [ 'if form could not set NWK frame counter', @@ -1045,7 +1046,7 @@ describe('Ember Adapter Layer', () => { () => { writeFileSync(backupPath, 'abcd'); }, - `[BACKUP] Coordinator backup is corrupted.`, + `Coordinator backup is corrupted (SyntaxError: Unexpected token 'a', \"abcd\" is not valid JSON)`, ], [ 'if backup unsupported', @@ -1056,7 +1057,7 @@ describe('Ember Adapter Layer', () => { writeFileSync(backupPath, JSON.stringify(customBackup, undefined, 2)); }, - `[BACKUP] Unsupported open coordinator backup version (version=2).`, + `Coordinator backup is corrupted (Error: Unsupported open coordinator backup version (version=2))`, ], [ 'if backup not EmberZNet stack specific', @@ -1069,10 +1070,10 @@ describe('Ember Adapter Layer', () => { `[BACKUP] Current backup file is not for EmberZNet stack.`, ], [ - 'if backup not EmberZNet EZSP version', + 'if backup is missing EmberZNet tclk', () => { const customBackup = deepClone(DEFAULT_BACKUP); - customBackup.metadata.internal.ezspVersion = undefined; + customBackup.stack_specific!.ezsp!.hashed_tclk = undefined; writeFileSync(backupPath, JSON.stringify(customBackup, undefined, 2)); }, @@ -1087,7 +1088,7 @@ describe('Ember Adapter Layer', () => { writeFileSync(backupPath, JSON.stringify(customBackup, undefined, 2)); }, - `[BACKUP] Unknown backup format.`, + `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); @@ -1178,23 +1179,6 @@ describe('Ember Adapter Layer', () => { ); }); - 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 jest.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); @@ -1207,28 +1191,6 @@ describe('Ember Adapter Layer', () => { 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 jest.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); @@ -1288,9 +1250,9 @@ describe('Ember Adapter Layer', () => { it('Throws when failed to retrieve parameter from NCP', async () => { mockEzspGetNetworkParameters - .mockResolvedValueOnce([SLStatus.FAIL, 0, {}]) - .mockResolvedValueOnce([SLStatus.FAIL, 0, {}]) - .mockResolvedValueOnce([SLStatus.FAIL, 0, {}]); + .mockResolvedValueOnce([SLStatus.FAIL, 0, deepClone(DEFAULT_ADAPTER_NETWORK_PARAMETERS)]) + .mockResolvedValueOnce([SLStatus.FAIL, 0, deepClone(DEFAULT_ADAPTER_NETWORK_PARAMETERS)]) + .mockResolvedValueOnce([SLStatus.FAIL, 0, deepClone(DEFAULT_ADAPTER_NETWORK_PARAMETERS)]); adapter.clearNetworkCache(); @@ -2176,21 +2138,21 @@ describe('Ember Adapter Layer', () => { [ 'failed get network parameters', () => { - mockEzspGetNetworkParameters.mockResolvedValueOnce([SLStatus.FAIL, 0, {}]); + mockEzspGetNetworkParameters.mockResolvedValueOnce([SLStatus.FAIL, 0, deepClone(DEFAULT_ADAPTER_NETWORK_PARAMETERS)]); }, `[BACKUP] Failed to get network parameters with status=FAIL.`, ], [ 'failed get network key info', () => { - mockEzspGetNetworkKeyInfo.mockResolvedValueOnce([SLStatus.FAIL, {}]); + mockEzspGetNetworkKeyInfo.mockResolvedValueOnce([SLStatus.FAIL, deepClone(DEFAULT_NETWORK_KEY_INFO)]); }, `[BACKUP] Failed to get network keys info with status=FAIL.`, ], // [ // 'failed get TC APS key info', // () => { - // mockEzspGetNetworkKeyInfo.mockResolvedValueOnce([SLStatus.FAIL, {}]); + // mockEzspGetNetworkKeyInfo.mockResolvedValueOnce([SLStatus.FAIL, deepClone(DEFAULT_NETWORK_KEY_INFO)]); // }, // `[BACKUP] Failed to get TC APS key info with status=FAIL.`, // ], @@ -2213,7 +2175,7 @@ describe('Ember Adapter Layer', () => { [ 'failed export TC link key', () => { - mockEzspExportKey.mockResolvedValueOnce([SLStatus.FAIL, {}]); + mockEzspExportKey.mockResolvedValueOnce([SLStatus.FAIL, {contents: Buffer.alloc(16)}]); }, `[BACKUP] Failed to export TC Link Key with status=FAIL.`, ], @@ -2225,7 +2187,7 @@ describe('Ember Adapter Layer', () => { SLStatus.OK, {contents: Buffer.from(DEFAULT_BACKUP.stack_specific!.ezsp!.hashed_tclk!, 'hex')} as SecManKey, ]) - .mockResolvedValueOnce([SLStatus.FAIL, {}]); + .mockResolvedValueOnce([SLStatus.FAIL, {contents: Buffer.alloc(16)}]); }, `[BACKUP] Failed to export Network Key with status=FAIL.`, ], @@ -2360,7 +2322,7 @@ describe('Ember Adapter Layer', () => { await flushPromises(); }, 300); - return [SLStatus.OK, ++mockAPSSequence]; + return Promise.resolve([SLStatus.OK, ++mockAPSSequence]); }; mockEzspSendUnicast.mockImplementationOnce(emitResponse).mockImplementationOnce(emitResponse); @@ -2522,7 +2484,7 @@ describe('Ember Adapter Layer', () => { await flushPromises(); }, 300); - return [SLStatus.OK, ++mockAPSSequence]; + return Promise.resolve([SLStatus.OK, ++mockAPSSequence]); }); const p = adapter.sendZclFrameToEndpoint('0x1122334455667788', networkAddress, endpoint, zclFrame, 10000, false, false, sourceEndpoint); @@ -2583,7 +2545,7 @@ describe('Ember Adapter Layer', () => { await flushPromises(); }, 300); - return [SLStatus.OK, ++mockAPSSequence]; + return Promise.resolve([SLStatus.OK, ++mockAPSSequence]); }); const p = adapter.sendZclFrameToEndpoint('0x1122334455667788', networkAddress, endpoint, zclFrame, 10000, false, false, sourceEndpoint); @@ -2644,7 +2606,7 @@ describe('Ember Adapter Layer', () => { await flushPromises(); }, 300); - return [SLStatus.OK, ++mockAPSSequence]; + return Promise.resolve([SLStatus.OK, ++mockAPSSequence]); }); const p = adapter.sendZclFrameToEndpoint('0x1122334455667788', networkAddress, endpoint, zclFrame, 10000, false, false); @@ -2708,7 +2670,7 @@ describe('Ember Adapter Layer', () => { await flushPromises(); }, 300); - return [SLStatus.OK, ++mockAPSSequence]; + return Promise.resolve([SLStatus.OK, ++mockAPSSequence]); }); const p = adapter.sendZclFrameToEndpoint('0x1122334455667788', networkAddress, endpoint, zclFrame, 10000, false, false, sourceEndpoint); @@ -2774,7 +2736,7 @@ describe('Ember Adapter Layer', () => { await flushPromises(); }, 300); - return [SLStatus.OK, ++mockAPSSequence]; + return Promise.resolve([SLStatus.OK, ++mockAPSSequence]); }); const p = adapter.sendZclFrameToEndpoint('0x1122334455667788', networkAddress, endpoint, zclFrame, 10000, false, false, sourceEndpoint); @@ -3027,7 +2989,7 @@ describe('Ember Adapter Layer', () => { await flushPromises(); }, 300); - return [SLStatus.OK, ++mockAPSSequence]; + return Promise.resolve([SLStatus.OK, ++mockAPSSequence]); }); const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, senderEUI64, false, 0); @@ -3092,7 +3054,7 @@ describe('Ember Adapter Layer', () => { await flushPromises(); }, 300); - return [SLStatus.OK, ++mockAPSSequence]; + return Promise.resolve([SLStatus.OK, ++mockAPSSequence]); }); const p = adapter.sendZclFrameToEndpoint('0x1122334455667788', networkAddress, endpoint, zclFrame, 10000, true, false, sourceEndpoint); @@ -3349,7 +3311,7 @@ describe('Ember Adapter Layer', () => { await flushPromises(); }, 300); - return SLStatus.OK; + return Promise.resolve(SLStatus.OK); }); const p = adapter.sendZclFrameInterPANBroadcast(zclFrame, 10000); From dc34b9c60f5a3b27bde3e2f2a282b122e0e0cac8 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Wed, 11 Dec 2024 19:45:16 +0100 Subject: [PATCH 02/16] cleanup --- src/adapter/ember/adapter/emberAdapter.ts | 47 ++++++++++------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index a7745944df..6e671d039d 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -696,7 +696,8 @@ export class EmberAdapter extends Adapter { await this.registerFixedEndpoints(); this.clearNetworkCache(); - result = await this.initTrustCenter(); + await this.initTrustCenter(); + result = await this.initNetwork(); // after network UP, as per SDK, ensures clean slate await this.initNCPConcentrator(); @@ -809,39 +810,31 @@ export class EmberAdapter extends Adapter { } } - /** - * - * @returns True if the network needed to be formed. - */ - private async initTrustCenter(): Promise { + private async initTrustCenter(): Promise { // init TC policies - { - let status = await this.emberSetEzspPolicy(EzspPolicyId.TC_KEY_REQUEST_POLICY, EzspDecisionId.ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY); + let status = await this.emberSetEzspPolicy(EzspPolicyId.TC_KEY_REQUEST_POLICY, EzspDecisionId.ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY); - if (status !== SLStatus.OK) { - throw new Error( - `[INIT TC] Failed to set EzspPolicyId TC_KEY_REQUEST_POLICY to ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY with status=${SLStatus[status]}.`, - ); - } + if (status !== SLStatus.OK) { + throw new Error( + `[INIT TC] Failed to set EzspPolicyId TC_KEY_REQUEST_POLICY to ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY with status=${SLStatus[status]}.`, + ); + } - /* istanbul ignore next */ - const appKeyRequestsPolicy = ALLOW_APP_KEY_REQUESTS ? EzspDecisionId.ALLOW_APP_KEY_REQUESTS : EzspDecisionId.DENY_APP_KEY_REQUESTS; - status = await this.emberSetEzspPolicy(EzspPolicyId.APP_KEY_REQUEST_POLICY, appKeyRequestsPolicy); + /* istanbul ignore next */ + const appKeyRequestsPolicy = ALLOW_APP_KEY_REQUESTS ? EzspDecisionId.ALLOW_APP_KEY_REQUESTS : EzspDecisionId.DENY_APP_KEY_REQUESTS; + status = await this.emberSetEzspPolicy(EzspPolicyId.APP_KEY_REQUEST_POLICY, appKeyRequestsPolicy); - if (status !== SLStatus.OK) { - throw new Error( - `[INIT TC] Failed to set EzspPolicyId APP_KEY_REQUEST_POLICY to ${EzspDecisionId[appKeyRequestsPolicy]} with status=${SLStatus[status]}.`, - ); - } + if (status !== SLStatus.OK) { + throw new Error( + `[INIT TC] Failed to set EzspPolicyId APP_KEY_REQUEST_POLICY to ${EzspDecisionId[appKeyRequestsPolicy]} with status=${SLStatus[status]}.`, + ); + } - status = await this.emberSetJoinPolicy(EmberJoinDecision.USE_PRECONFIGURED_KEY); + status = await this.emberSetJoinPolicy(EmberJoinDecision.USE_PRECONFIGURED_KEY); - if (status !== SLStatus.OK) { - throw new Error(`[INIT TC] Failed to set join policy to USE_PRECONFIGURED_KEY with status=${SLStatus[status]}.`); - } + if (status !== SLStatus.OK) { + throw new Error(`[INIT TC] Failed to set join policy to USE_PRECONFIGURED_KEY with status=${SLStatus[status]}.`); } - - return await this.initNetwork(); } /** From 50a7d3cbc647c4768727b4b3f30b19a1bb21ffd2 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Wed, 11 Dec 2024 22:12:41 +0100 Subject: [PATCH 03/16] prelim deconz --- src/adapter/deconz/adapter/deconzAdapter.ts | 182 +++++--------------- 1 file changed, 46 insertions(+), 136 deletions(-) diff --git a/src/adapter/deconz/adapter/deconzAdapter.ts b/src/adapter/deconz/adapter/deconzAdapter.ts index 86931eed68..a27d4cdfdf 100644 --- a/src/adapter/deconz/adapter/deconzAdapter.ts +++ b/src/adapter/deconz/adapter/deconzAdapter.ts @@ -83,159 +83,69 @@ class DeconzAdapter extends Adapter { const baudrate = this.serialPortOptions.baudRate || 38400; await this.driver.open(baudrate); - let changed: boolean = false; - const panid = await this.driver.readParameterRequest(PARAM.PARAM.Network.PAN_ID); - const expanid = await this.driver.readParameterRequest(PARAM.PARAM.Network.APS_EXT_PAN_ID); - const channel = await this.driver.readParameterRequest(PARAM.PARAM.Network.CHANNEL); - const networkKey = await this.driver.readParameterRequest(PARAM.PARAM.Network.NETWORK_KEY); - - // check current channel against configuration.yaml - if (this.networkOptions.channelList[0] !== channel) { - logger.debug( - 'Channel in configuration.yaml (' + - this.networkOptions.channelList[0] + - ') differs from current channel (' + - channel + - '). Changing channel.', - NS, - ); - - let setChannelMask = 0; - switch (this.networkOptions.channelList[0]) { - case 11: - setChannelMask = 0x800; - break; - case 12: - setChannelMask = 0x1000; - break; - case 13: - setChannelMask = 0x2000; - break; - case 14: - setChannelMask = 0x4000; - break; - case 15: - setChannelMask = 0x8000; - break; - case 16: - setChannelMask = 0x10000; - break; - case 17: - setChannelMask = 0x20000; - break; - case 18: - setChannelMask = 0x40000; - break; - case 19: - setChannelMask = 0x80000; - break; - case 20: - setChannelMask = 0x100000; - break; - case 21: - setChannelMask = 0x200000; - break; - case 22: - setChannelMask = 0x400000; - break; - case 23: - setChannelMask = 0x800000; - break; - case 24: - setChannelMask = 0x1000000; - break; - case 25: - setChannelMask = 0x2000000; - break; - case 26: - setChannelMask = 0x4000000; - break; - default: - break; - } + const result = await this.initNetwork(); - try { - await this.driver.writeParameterRequest(PARAM.PARAM.Network.CHANNEL_MASK, setChannelMask); - await Wait(500); - changed = true; - } catch (error) { - logger.debug('Could not set channel: ' + error, NS); - } - } + // write endpoints + //[ sd1 ep proId devId vers #inCl iCl1 iCl2 iCl3 iCl4 iCl5 #outC oCl1 oCl2 oCl3 oCl4 ] + const sd = [ + 0x00, 0x01, 0x04, 0x01, 0x05, 0x00, 0x01, 0x05, 0x00, 0x00, 0x00, 0x06, 0x0a, 0x00, 0x19, 0x00, 0x01, 0x05, 0x04, 0x01, 0x00, 0x20, 0x00, + 0x00, 0x05, 0x02, 0x05, + ]; + const sd1 = sd.reverse(); + await this.driver.writeParameterRequest(PARAM.PARAM.STK.Endpoint, sd1); - // check current panid against configuration.yaml - if (this.networkOptions.panID !== panid) { - logger.debug( - 'panid in configuration.yaml (' + this.networkOptions.panID + ') differs from current panid (' + panid + '). Changing panid.', - NS, - ); + return result; + } - try { - await this.driver.writeParameterRequest(PARAM.PARAM.Network.PAN_ID, this.networkOptions.panID); - await Wait(500); - changed = true; - } catch (error) { - logger.debug('Could not set panid: ' + error, NS); - } - } + public async stop(): Promise { + await this.driver.close(); + } - // check current extended_panid against configuration.yaml - if (this.driver.generalArrayToString(this.networkOptions.extendedPanID!, 8) !== expanid) { - logger.debug( - 'extended panid in configuration.yaml (' + - this.driver.macAddrArrayToString(this.networkOptions.extendedPanID!) + - ') differs from current extended panid (' + - expanid + - '). Changing extended panid.', - NS, - ); + public async hasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { + const panid = (await this.driver.readParameterRequest(PARAM.PARAM.Network.PAN_ID)) as number; + const expanid = (await this.driver.readParameterRequest(PARAM.PARAM.Network.APS_EXT_PAN_ID)) as string; - try { - await this.driver.writeParameterRequest(PARAM.PARAM.Network.APS_EXT_PAN_ID, this.networkOptions.extendedPanID!); - await Wait(500); - changed = true; - } catch (error) { - logger.debug('Could not set extended panid: ' + error, NS); - } + // TODO: probably should be a request for actual NET_CONNECTED NetworkState instead? + if (panid && expanid) { + return [true, panid, Buffer.from(expanid, 'hex')]; } - // check current network key against configuration.yaml - if (this.driver.generalArrayToString(this.networkOptions.networkKey!, 16) !== networkKey) { - logger.debug( - 'network key in configuration.yaml (hidden) differs from current network key (' + networkKey + '). Changing network key.', - NS, - ); + return [false, undefined, undefined]; + } - try { - await this.driver.writeParameterRequest(PARAM.PARAM.Network.NETWORK_KEY, this.networkOptions.networkKey!); - await Wait(500); - changed = true; - } catch (error) { - logger.debug('Could not set network key: ' + error, NS); - } - } + public async leaveNetwork(): Promise { + // TODO: https://github.com/dresden-elektronik/deconz/blob/7a87ce0b3e401dd1be89b10e41b13abfd983e329/src/zm_controller.cpp#L3130-L3147 + // since this is used below AFTER changing parameters, this does not seem to actually "clear" the network in the adapter, just puts it in a state of "offline"? + await this.driver.changeNetworkStateRequest(PARAM.PARAM.Network.NET_OFFLINE); + await Wait(2000); + } + + public async formNetwork(backup?: Models.Backup): Promise { + if (backup) { + throw new Error('This adapter does not support backup'); + } else { + await this.driver.writeParameterRequest(PARAM.PARAM.Network.PAN_ID, this.networkOptions.panID); + await Wait(500); + await this.driver.writeParameterRequest(PARAM.PARAM.Network.APS_EXT_PAN_ID, this.networkOptions.extendedPanID!); + await Wait(500); + await this.driver.writeParameterRequest(PARAM.PARAM.Network.NETWORK_KEY, this.networkOptions.networkKey!); + await Wait(500); - if (changed) { await this.driver.changeNetworkStateRequest(PARAM.PARAM.Network.NET_OFFLINE); await Wait(2000); await this.driver.changeNetworkStateRequest(PARAM.PARAM.Network.NET_CONNECTED); await Wait(2000); } + } - // write endpoints - //[ sd1 ep proId devId vers #inCl iCl1 iCl2 iCl3 iCl4 iCl5 #outC oCl1 oCl2 oCl3 oCl4 ] - const sd = [ - 0x00, 0x01, 0x04, 0x01, 0x05, 0x00, 0x01, 0x05, 0x00, 0x00, 0x00, 0x06, 0x0a, 0x00, 0x19, 0x00, 0x01, 0x05, 0x04, 0x01, 0x00, 0x20, 0x00, - 0x00, 0x05, 0x02, 0x05, - ]; - const sd1 = sd.reverse(); - await this.driver.writeParameterRequest(PARAM.PARAM.STK.Endpoint, sd1); - - return 'resumed'; + public async getNetworkKey(): Promise { + return Buffer.from((await this.driver.readParameterRequest(PARAM.PARAM.Network.NETWORK_KEY)) as string, 'hex'); } - public async stop(): Promise { - await this.driver.close(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public checkBackup(backup: Models.Backup): void { + // always fail if found a backup, since not supported, it can't be for deconz + throw new Error('This adapter does not support backup'); } public async getCoordinatorIEEE(): Promise { From 5575a93de58317d2961a31e016fea5ed4a9d126f Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Wed, 11 Dec 2024 23:47:19 +0100 Subject: [PATCH 04/16] fixes --- src/adapter/adapter.ts | 8 ++- src/adapter/deconz/adapter/deconzAdapter.ts | 3 +- src/adapter/ember/adapter/emberAdapter.ts | 57 ++++++++++----------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index e97c50e899..67d414e557 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -99,7 +99,7 @@ abstract class Adapter extends events.EventEmitter { FORM_BACKUP, } - const [hasNetwork, panID, extendedPanID] = await this.hasNetwork(); + const [hasNetwork, panID, extendedPanID] = await this.initHasNetwork(); const extendedPanIDBuffer = Buffer.from(this.networkOptions.extendedPanID); const configNetworkKeyBuffer = Buffer.from(this.networkOptions.networkKey!); let action: InitAction = InitAction.DONE; @@ -174,7 +174,11 @@ abstract class Adapter extends events.EventEmitter { public abstract stop(): Promise; - public abstract hasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]>; + /** + * Check the network status on the adapter (execute the necessary pre-steps to be able to get it). + * WARNING: This is a one-off. Should not be called outside of `initNetwork`. + */ + protected abstract initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]>; public abstract leaveNetwork(): Promise; diff --git a/src/adapter/deconz/adapter/deconzAdapter.ts b/src/adapter/deconz/adapter/deconzAdapter.ts index a27d4cdfdf..5618bc7c4f 100644 --- a/src/adapter/deconz/adapter/deconzAdapter.ts +++ b/src/adapter/deconz/adapter/deconzAdapter.ts @@ -101,7 +101,7 @@ class DeconzAdapter extends Adapter { await this.driver.close(); } - public async hasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { + protected async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { const panid = (await this.driver.readParameterRequest(PARAM.PARAM.Network.PAN_ID)) as number; const expanid = (await this.driver.readParameterRequest(PARAM.PARAM.Network.APS_EXT_PAN_ID)) as string; @@ -122,6 +122,7 @@ class DeconzAdapter extends Adapter { public async formNetwork(backup?: Models.Backup): Promise { if (backup) { + // this path should never be reached throw new Error('This adapter does not support backup'); } else { await this.driver.writeParameterRequest(PARAM.PARAM.Network.PAN_ID, this.networkOptions.panID); diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 6e671d039d..b5dd9b87d4 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -1320,47 +1320,42 @@ export class EmberAdapter extends Adapter { logger.info(`======== Ember Adapter Stopped ========`, NS); } - public async hasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { - // when called after init TC, should always return true - if (this.networkCache.parameters.panId !== ZSpec.INVALID_PAN_ID) { - return [true, this.networkCache.parameters.panId, Buffer.from(this.networkCache.parameters.extendedPanId)]; - } else { - const networkInitStruct: EmberNetworkInitStruct = { - bitmask: EmberNetworkInitBitmask.PARENT_INFO_IN_TOKEN | EmberNetworkInitBitmask.END_DEVICE_REJOIN_ON_REBOOT, - }; - const initStatus = await this.ezsp.ezspNetworkInit(networkInitStruct); - - logger.debug(`Network init status=${SLStatus[initStatus]}.`, NS); + protected async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { + const networkInitStruct: EmberNetworkInitStruct = { + bitmask: EmberNetworkInitBitmask.PARENT_INFO_IN_TOKEN | EmberNetworkInitBitmask.END_DEVICE_REJOIN_ON_REBOOT, + }; + const initStatus = await this.ezsp.ezspNetworkInit(networkInitStruct); - if (initStatus !== SLStatus.OK && initStatus !== SLStatus.NOT_JOINED) { - throw new Error(`Failed network init request with status=${SLStatus[initStatus]}.`); - } + logger.debug(`Network init status=${SLStatus[initStatus]}.`, NS); - if (initStatus === SLStatus.OK) { - await this.oneWaitress.startWaitingForEvent( - {eventName: OneWaitressEvents.STACK_STATUS_NETWORK_UP}, - DEFAULT_NETWORK_REQUEST_TIMEOUT, - 'Network init', - ); + if (initStatus !== SLStatus.OK && initStatus !== SLStatus.NOT_JOINED) { + throw new Error(`Failed network init request with status=${SLStatus[initStatus]}.`); + } - const [npStatus, nodeType, netParams] = await this.ezsp.ezspGetNetworkParameters(); + if (initStatus === SLStatus.OK) { + await this.oneWaitress.startWaitingForEvent( + {eventName: OneWaitressEvents.STACK_STATUS_NETWORK_UP}, + DEFAULT_NETWORK_REQUEST_TIMEOUT, + 'Network init', + ); - if (npStatus !== SLStatus.OK) { - throw new Error(`Failed to get network parameters with status=${SLStatus[npStatus]}.`); - } + const [npStatus, nodeType, netParams] = await this.ezsp.ezspGetNetworkParameters(); - logger.debug(() => `Current adapter network: nodeType=${EmberNodeType[nodeType]} params=${JSON.stringify(netParams)}`, NS); + if (npStatus !== SLStatus.OK) { + throw new Error(`Failed to get network parameters with status=${SLStatus[npStatus]}.`); + } - // force forming in case current network is `ROUTER` type - if (nodeType !== EmberNodeType.COORDINATOR) { - return [false, undefined, undefined]; - } + logger.debug(() => `Current adapter network: nodeType=${EmberNodeType[nodeType]} params=${JSON.stringify(netParams)}`, NS); - return [true, netParams.panId, Buffer.from(netParams.extendedPanId)]; + // force forming in case current network is `ROUTER` type + if (nodeType !== EmberNodeType.COORDINATOR) { + return [false, undefined, undefined]; } - return [false, undefined, undefined]; + return [true, netParams.panId, Buffer.from(netParams.extendedPanId)]; } + + return [false, undefined, undefined]; } public async leaveNetwork(): Promise { From b765b0233ebc5d6cf7c0f3ed9a26c7087337a993 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Wed, 11 Dec 2024 23:48:39 +0100 Subject: [PATCH 05/16] prelim zigate --- src/adapter/zigate/adapter/zigateAdapter.ts | 137 +++++++++++--------- 1 file changed, 73 insertions(+), 64 deletions(-) diff --git a/src/adapter/zigate/adapter/zigateAdapter.ts b/src/adapter/zigate/adapter/zigateAdapter.ts index dc4a43de32..e412d0f543 100644 --- a/src/adapter/zigate/adapter/zigateAdapter.ts +++ b/src/adapter/zigate/adapter/zigateAdapter.ts @@ -66,40 +66,27 @@ class ZiGateAdapter extends Adapter { * Adapter methods */ public async start(): Promise { - let startResult: TsType.StartResult = 'resumed'; - try { - await this.driver.open(); - logger.info('Connected to ZiGate adapter successfully.', NS); - - const resetResponse = await this.driver.sendCommand(ZiGateCommandCode.Reset, {}, 5000); - if (resetResponse.code === ZiGateMessageCode.RestartNonFactoryNew) { - startResult = 'resumed'; - } else if (resetResponse.code === ZiGateMessageCode.RestartFactoryNew) { - startResult = 'reset'; - } - await this.driver.sendCommand(ZiGateCommandCode.RawMode, {enabled: 0x01}); - // @todo check - await this.driver.sendCommand(ZiGateCommandCode.SetDeviceType, { - deviceType: DEVICE_TYPE.coordinator, - }); - await this.initNetwork(); - - await this.driver.sendCommand(ZiGateCommandCode.AddGroup, { - addressMode: ADDRESS_MODE.short, - shortAddress: ZSpec.COORDINATOR_ADDRESS, - sourceEndpoint: ZSpec.HA_ENDPOINT, - destinationEndpoint: ZSpec.HA_ENDPOINT, - groupAddress: default_bind_group, - }); + await this.driver.open(); + logger.info('Connected to ZiGate adapter successfully.', NS); - if (this.adapterOptions.transmitPower != undefined) { - await this.driver.sendCommand(ZiGateCommandCode.SetTXpower, {value: this.adapterOptions.transmitPower}); - } - } catch (error) { - throw new Error('failed to connect to zigate adapter ' + (error as Error).message); + // TODO: should always be called or only in `formNetwork`? this is not in doc API + await this.driver.sendCommand(ZiGateCommandCode.RawMode, {enabled: 0x01}); + + const result = await this.initNetwork(); + + await this.driver.sendCommand(ZiGateCommandCode.AddGroup, { + addressMode: ADDRESS_MODE.short, + shortAddress: ZSpec.COORDINATOR_ADDRESS, + sourceEndpoint: ZSpec.HA_ENDPOINT, + destinationEndpoint: ZSpec.HA_ENDPOINT, + groupAddress: default_bind_group, + }); + + if (this.adapterOptions.transmitPower != undefined) { + await this.driver.sendCommand(ZiGateCommandCode.SetTXpower, {value: this.adapterOptions.transmitPower}); } - return startResult; // 'resumed' | 'reset' | 'restored' + return result; } public async stop(): Promise { @@ -107,6 +94,58 @@ class ZiGateAdapter extends Adapter { await this.driver.close(); } + protected async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { + const resetResponse = await this.driver.sendCommand(ZiGateCommandCode.Reset, {}, 5000); + + if (resetResponse.code === ZiGateMessageCode.RestartNonFactoryNew) { + const result = await this.driver.sendCommand(ZiGateCommandCode.GetNetworkState, {}, 10000); + const extPanId = result.payload.ExtPANID as number; + const extPanIdBuf = Buffer.from(extPanId.toString(16), 'hex'); + + return [true, result.payload.PANID as number, extPanIdBuf]; + } + + return [false, undefined, undefined]; + } + + public async leaveNetwork(): Promise { + await this.driver.sendCommand(ZiGateCommandCode.ErasePersistentData, {}, 5000); + // will be ZiGateMessageCode.RestartFactoryNew when here + } + + public async formNetwork(backup?: Models.Backup): Promise { + if (backup) { + // this path should never be reached + throw new Error('This adapter does not support backup'); + } else { + await this.driver.sendCommand(ZiGateCommandCode.SetDeviceType, {deviceType: DEVICE_TYPE.coordinator}); + await this.driver.sendCommand(ZiGateCommandCode.SetChannelMask, { + channelMask: ZSpec.Utils.channelsToUInt32Mask(this.networkOptions.channelList), + }); + await this.driver.sendCommand(ZiGateCommandCode.SetSecurityStateKey, { + keyType: this.networkOptions.networkKeyDistribute + ? ZPSNwkKeyState.ZPS_ZDO_DISTRIBUTED_LINK_KEY + : ZPSNwkKeyState.ZPS_ZDO_PRECONFIGURED_LINK_KEY, + key: this.networkOptions.networkKey, + }); + logger.debug(`Set EPanID ${this.networkOptions.extendedPanID.toString()}`, NS); + await this.driver.sendCommand(ZiGateCommandCode.SetExtendedPANID, {panId: this.networkOptions.extendedPanID}); + await this.driver.sendCommand(ZiGateCommandCode.StartNetwork, {}); + } + } + + public async getNetworkKey(): Promise { + // TODO: doesn't look like zigate has any way to retrieve network key... + // force 'always assume matching' + return Buffer.from(this.networkOptions.networkKey); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public checkBackup(backup: Models.Backup): void { + // always fail if found a backup, since not supported, it can't be for zigate + throw new Error('This adapter does not support backup'); + } + public async getCoordinatorIEEE(): Promise { const networkResponse = await this.driver.sendCommand(ZiGateCommandCode.GetNetworkState); return networkResponse.payload.extendedAddress; @@ -173,9 +212,9 @@ class ZiGateAdapter extends Adapter { const result = await this.driver.sendCommand(ZiGateCommandCode.GetNetworkState, {}, 10000); return { - panID: result.payload.PANID, - extendedPanID: result.payload.ExtPANID, - channel: result.payload.Channel, + panID: result.payload.PANID as number, + extendedPanID: result.payload.ExtPANID as number, + channel: result.payload.Channel as number, }; } catch (error) { throw new Error(`Get network parameters failed ${error}`); @@ -472,36 +511,6 @@ class ZiGateAdapter extends Adapter { }); } - /** - * Supplementary functions - */ - private async initNetwork(): Promise { - logger.debug(`Set channel mask ${this.networkOptions.channelList} key`, NS); - await this.driver.sendCommand(ZiGateCommandCode.SetChannelMask, { - channelMask: ZSpec.Utils.channelsToUInt32Mask(this.networkOptions.channelList), - }); - - logger.debug(`Set security key`, NS); - await this.driver.sendCommand(ZiGateCommandCode.SetSecurityStateKey, { - keyType: this.networkOptions.networkKeyDistribute - ? ZPSNwkKeyState.ZPS_ZDO_DISTRIBUTED_LINK_KEY - : ZPSNwkKeyState.ZPS_ZDO_PRECONFIGURED_LINK_KEY, - key: this.networkOptions.networkKey, - }); - - try { - // The block is wrapped in trapping because if the network is already created, the firmware does not accept the new key. - logger.debug(`Set EPanID ${this.networkOptions.extendedPanID!.toString()}`, NS); - await this.driver.sendCommand(ZiGateCommandCode.SetExtendedPANID, { - panId: this.networkOptions.extendedPanID, - }); - - await this.driver.sendCommand(ZiGateCommandCode.StartNetwork, {}); - } catch (error) { - logger.error((error as Error).stack!, NS); - } - } - public waitFor( networkAddress: number | undefined, endpoint: number, From cb3b7dcba66b066a35ce0dfbbbb250b1fa370ab8 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:46:09 +0100 Subject: [PATCH 06/16] add comments --- src/adapter/adapter.ts | 3 +++ src/adapter/deconz/adapter/deconzAdapter.ts | 4 ++++ src/adapter/ember/adapter/emberAdapter.ts | 4 ++++ src/adapter/zigate/adapter/zigateAdapter.ts | 4 ++++ 4 files changed, 15 insertions(+) diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index 67d414e557..f010c5e636 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -182,6 +182,9 @@ abstract class Adapter extends events.EventEmitter { public abstract leaveNetwork(): Promise; + /** + * If backup is defined, form network from backup, otherwise from config. + */ public abstract formNetwork(backup?: Models.Backup): Promise; public abstract getNetworkKey(): Promise; diff --git a/src/adapter/deconz/adapter/deconzAdapter.ts b/src/adapter/deconz/adapter/deconzAdapter.ts index 87acc4d0eb..bea48cc2da 100644 --- a/src/adapter/deconz/adapter/deconzAdapter.ts +++ b/src/adapter/deconz/adapter/deconzAdapter.ts @@ -101,6 +101,10 @@ class DeconzAdapter extends Adapter { await this.driver.close(); } + /** + * Check the network status on the adapter (execute the necessary pre-steps to be able to get it). + * WARNING: This is a one-off. Should not be called outside of `initNetwork`. + */ protected async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { const panid = (await this.driver.readParameterRequest(PARAM.PARAM.Network.PAN_ID)) as number; const expanid = (await this.driver.readParameterRequest(PARAM.PARAM.Network.APS_EXT_PAN_ID)) as string; diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 84ffeb8c9e..7c7da45d6e 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -1320,6 +1320,10 @@ export class EmberAdapter extends Adapter { logger.info(`======== Ember Adapter Stopped ========`, NS); } + /** + * Check the network status on the adapter (execute the necessary pre-steps to be able to get it). + * WARNING: This is a one-off. Should not be called outside of `initNetwork`. + */ protected async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { const networkInitStruct: EmberNetworkInitStruct = { bitmask: EmberNetworkInitBitmask.PARENT_INFO_IN_TOKEN | EmberNetworkInitBitmask.END_DEVICE_REJOIN_ON_REBOOT, diff --git a/src/adapter/zigate/adapter/zigateAdapter.ts b/src/adapter/zigate/adapter/zigateAdapter.ts index 1706e1cf7a..e44fc02a04 100644 --- a/src/adapter/zigate/adapter/zigateAdapter.ts +++ b/src/adapter/zigate/adapter/zigateAdapter.ts @@ -94,6 +94,10 @@ class ZiGateAdapter extends Adapter { await this.driver.close(); } + /** + * Check the network status on the adapter (execute the necessary pre-steps to be able to get it). + * WARNING: This is a one-off. Should not be called outside of `initNetwork`. + */ protected async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { const resetResponse = await this.driver.sendCommand(ZiGateCommandCode.Reset, {}, 5000); From bf3f2fc04ecda022a2775f725145b92b85941282 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:12:04 +0100 Subject: [PATCH 07/16] prelim ezsp --- src/adapter/ezsp/adapter/ezspAdapter.ts | 116 ++++++++++++++- src/adapter/ezsp/driver/driver.ts | 184 ++--------------------- src/adapter/ezsp/driver/ezsp.ts | 8 +- src/adapter/ezsp/driver/types/struct.ts | 186 ++++++++---------------- 4 files changed, 188 insertions(+), 306 deletions(-) diff --git a/src/adapter/ezsp/adapter/ezspAdapter.ts b/src/adapter/ezsp/adapter/ezspAdapter.ts index 1c0bc3965d..aebd8e1468 100644 --- a/src/adapter/ezsp/adapter/ezspAdapter.ts +++ b/src/adapter/ezsp/adapter/ezspAdapter.ts @@ -13,7 +13,20 @@ import Adapter from '../../adapter'; import {ZclPayload} from '../../events'; import {AdapterOptions, CoordinatorVersion, NetworkOptions, NetworkParameters, SerialPortOptions, StartResult} from '../../tstype'; import {Driver, EmberIncomingMessage} from '../driver'; -import {EmberEUI64, EmberStatus} from '../driver/types'; +import { + EmberEUI64, + EmberInitialSecurityBitmask, + EmberInitialSecurityState, + EmberJoinMethod, + EmberKeyData, + EmberKeyStruct, + EmberKeyType, + EmberNetworkParameters, + EmberNodeType, + EmberStatus, + EzspValueId, +} from '../driver/types'; +import {ember_security} from '../driver/utils'; const NS = 'zh:ezsp'; @@ -45,7 +58,7 @@ class EZSPAdapter extends Adapter { logger.debug(`Adapter concurrent: ${concurrent}`, NS); this.queue = new Queue(concurrent); - this.driver = new Driver(this.serialPortOptions, this.networkOptions, backupPath); + this.driver = new Driver(this.serialPortOptions, backupPath, this.initNetwork.bind(this)); this.driver.on('close', this.onDriverClose.bind(this)); this.driver.on('deviceJoined', this.handleDeviceJoin.bind(this)); this.driver.on('deviceLeft', this.handleDeviceLeft.bind(this)); @@ -142,7 +155,14 @@ class EZSPAdapter extends Adapter { `'ezsp' driver is deprecated and will only remain to provide support for older firmware (pre 7.4.x). Migration to 'ember' is recommended. If using Zigbee2MQTT see https://github.com/Koenkk/zigbee2mqtt/discussions/21462`, NS, ); - return await this.driver.startup(this.adapterOptions.transmitPower); + + const result = await this.driver.startup(); + + if (this.adapterOptions.transmitPower != undefined && this.driver.networkParams.radioTxPower !== this.adapterOptions.transmitPower) { + await this.driver.ezsp.execCommand('setRadioPower', {power: this.adapterOptions.transmitPower}); + } + + return result; } public async stop(): Promise { @@ -150,6 +170,90 @@ class EZSPAdapter extends Adapter { await this.driver.stop(); } + /** + * Check the network status on the adapter (execute the necessary pre-steps to be able to get it). + * WARNING: This is a one-off. Should not be called outside of `initNetwork`. + */ + protected async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { + const hasNetwork = await this.driver.ezsp.networkInit(); + + if (hasNetwork) { + const {status, nodeType, parameters} = await this.driver.ezsp.execCommand('getNetworkParameters'); + + if (status !== EmberStatus.SUCCESS) { + throw new Error(`Failed to get network parameters`); + } + + // force forming in case current network is `ROUTER` type + if (nodeType !== EmberNodeType.COORDINATOR) { + return [false, undefined, undefined]; + } + + logger.debug( + () => `Current adapter network: nodeType=${EmberNodeType.valueToName(EmberNodeType, nodeType)} params=${JSON.stringify(parameters)}`, + NS, + ); + + return [true, (parameters as EmberNetworkParameters).panId, (parameters as EmberNetworkParameters).extendedPanId]; + } + + return [false, undefined, undefined]; + } + + public async leaveNetwork(): Promise { + await this.driver.ezsp.leaveNetwork(); + } + + public async formNetwork(backup?: Models.Backup): Promise { + await this.driver.ezsp.execCommand('clearTransientLinkKeys'); + + if (backup) { + const initial_security_state: EmberInitialSecurityState = ember_security(backup.networkOptions.networkKey); + initial_security_state.bitmask |= EmberInitialSecurityBitmask.NO_FRAME_COUNTER_RESET; + initial_security_state.networkKeySequenceNumber = backup.networkKeyInfo.sequenceNumber; + // valid from `checkBackup` + initial_security_state.preconfiguredKey.contents = backup.ezsp!.hashed_tclk!; + + await this.driver.ezsp.setInitialSecurityState(initial_security_state); + } else { + await this.driver.ezsp.execCommand('clearKeyTable'); + const initial_security_state: EmberInitialSecurityState = ember_security(Buffer.from(this.networkOptions.networkKey)); + + await this.driver.ezsp.setInitialSecurityState(initial_security_state); + } + + const parameters: EmberNetworkParameters = new EmberNetworkParameters(); + parameters.radioTxPower = this.adapterOptions.transmitPower ?? 5; + parameters.joinMethod = EmberJoinMethod.USE_MAC_ASSOCIATION; + parameters.nwkManagerId = 0; + parameters.nwkUpdateId = 0; + parameters.channels = 0x07fff800; // all channels + + if (backup) { + parameters.panId = backup.networkOptions.panId; + parameters.extendedPanId = backup.networkOptions.extendedPanId; + parameters.radioChannel = backup.logicalChannel; + parameters.nwkUpdateId = backup.networkUpdateId; + } else { + parameters.radioChannel = this.networkOptions.channelList[0]; + parameters.panId = this.networkOptions.panID; + parameters.extendedPanId = Buffer.from(this.networkOptions.extendedPanID); + } + + await this.driver.ezsp.formNetwork(parameters); + await this.driver.ezsp.setValue(EzspValueId.VALUE_STACK_TOKEN_WRITING, 1); + } + + public async getNetworkKey(): Promise { + const networkKey = await this.driver.getKey(EmberKeyType.CURRENT_NETWORK_KEY); + + if (this.driver.ezsp.ezspV < 13) { + return Buffer.from((networkKey.keyStruct as EmberKeyStruct).key.contents); + } else { + return Buffer.from((networkKey.keyData as EmberKeyData).contents); + } + } + public async onDriverClose(): Promise { logger.debug(`onDriverClose()`, NS); @@ -470,6 +574,12 @@ class EZSPAdapter extends Adapter { return true; } + public checkBackup(backup: Models.Backup): void { + if (!backup.ezsp?.hashed_tclk) { + throw new Error(`Current backup file is not for EmberZNet stack.`); + } + } + public async backup(): Promise { assert(this.driver.ezsp.isInitialized(), 'Cannot make backup when ezsp is not initialized'); return await this.driver.backupMan.createBackup(); diff --git a/src/adapter/ezsp/driver/driver.ts b/src/adapter/ezsp/driver/driver.ts index f1bd4899a4..134a7531ae 100644 --- a/src/adapter/ezsp/driver/driver.ts +++ b/src/adapter/ezsp/driver/driver.ts @@ -2,8 +2,6 @@ import {EventEmitter} from 'events'; -import equals from 'fast-deep-equal/es6'; - import {Wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; import * as ZSpec from '../../../zspec'; @@ -15,15 +13,12 @@ import * as TsType from './../../tstype'; import {ParamsDesc} from './commands'; import {Ezsp, EZSPFrameData} from './ezsp'; import {Multicast} from './multicast'; -import {EmberApsOption, EmberJoinDecision, EmberKeyData, EmberNodeType, EmberStatus, uint8_t, uint16_t} from './types'; +import {EmberApsOption, EmberJoinDecision, EmberKeyData, EmberStatus, uint8_t, uint16_t} from './types'; import { EmberDerivedKeyType, EmberDeviceUpdate, EmberEUI64, - EmberInitialSecurityBitmask, - EmberJoinMethod, EmberKeyType, - EmberNetworkStatus, EmberOutgoingMessageType, EmberStackError, EzspDecisionBitmask, @@ -35,13 +30,10 @@ import { EmberAesMmoHashContext, EmberApsFrame, EmberIeeeRawFrame, - EmberInitialSecurityState, - EmberKeyStruct, EmberNetworkParameters, EmberRawFrame, EmberSecurityManagerContext, } from './types/struct'; -import {ember_security} from './utils'; const NS = 'zh:ezsp:driv'; @@ -94,13 +86,9 @@ const DEFAULT_MFG_ID = 0x1049; const REQUEST_ATTEMPT_DELAYS = [500, 1000, 1500]; export class Driver extends EventEmitter { - // @ts-expect-error XXX: init in startup - public ezsp: Ezsp; - private nwkOpt: TsType.NetworkOptions; - // @ts-expect-error XXX: init in startup - public networkParams: EmberNetworkParameters; - // @ts-expect-error XXX: init in startup - public version: { + public ezsp!: Ezsp; + public networkParams!: EmberNetworkParameters; + public version!: { product: number; majorrel: string; minorrel: string; @@ -109,22 +97,21 @@ export class Driver extends EventEmitter { }; private eui64ToNodeId = new Map(); // private eui64ToRelays = new Map(); - // @ts-expect-error XXX: init in startup - public ieee: EmberEUI64; - // @ts-expect-error XXX: init in startup - private multicast: Multicast; + public ieee!: EmberEUI64; + private multicast!: Multicast; private waitress: Waitress; private transactionID = 1; private serialOpt: TsType.SerialPortOptions; public backupMan: EZSPAdapterBackup; + private initNetwork: () => Promise; - constructor(serialOpt: TsType.SerialPortOptions, nwkOpt: TsType.NetworkOptions, backupPath: string) { + constructor(serialOpt: TsType.SerialPortOptions, backupPath: string, initNetwork: () => Promise) { super(); - this.nwkOpt = nwkOpt; this.serialOpt = serialOpt; this.waitress = new Waitress(this.waitressValidator, this.waitressTimeoutFormatter); this.backupMan = new EZSPAdapterBackup(this, backupPath); + this.initNetwork = initNetwork; } /** @@ -175,10 +162,8 @@ export class Driver extends EventEmitter { } } - public async startup(transmitPower?: number): Promise { - let result: TsType.StartResult = 'resumed'; + public async startup(): Promise { this.transactionID = 1; - // this.ezsp = undefined; this.ezsp = new Ezsp(); this.ezsp.on('close', this.onEzspClose.bind(this)); @@ -239,37 +224,7 @@ export class Driver extends EventEmitter { revision: vers, }; - if (await this.needsToBeInitialised(this.nwkOpt)) { - // need to check the backup - const restore = await this.needsToBeRestore(this.nwkOpt); - - const res = await this.ezsp.execCommand('networkState'); - - logger.debug(`Network state ${res.status}`, NS); - - if (res.status == EmberNetworkStatus.JOINED_NETWORK) { - logger.info(`Leaving current network and forming new network`, NS); - - const st = await this.ezsp.leaveNetwork(); - - if (st != EmberStatus.NETWORK_DOWN) { - logger.error(`leaveNetwork returned unexpected status: ${st}`, NS); - } - } - - if (restore) { - // restore - logger.info('Restore network from backup', NS); - await this.formNetwork(true, transmitPower); - result = 'restored'; - } else { - // reset - logger.info('Form network', NS); - await this.formNetwork(false, transmitPower); - result = 'reset'; - } - } - + const result = await this.initNetwork(); const state = (await this.ezsp.execCommand('networkState')).status; logger.debug(`Network state ${state}`, NS); @@ -301,71 +256,9 @@ export class Driver extends EventEmitter { await this.multicast.subscribe(ZSpec.GP_GROUP_ID, ZSpec.GP_ENDPOINT); // await this.multicast.subscribe(1, 901); - if (transmitPower != undefined && this.networkParams.radioTxPower !== transmitPower) { - await this.ezsp.execCommand('setRadioPower', {power: transmitPower}); - } - return result; } - private async needsToBeInitialised(options: TsType.NetworkOptions): Promise { - let valid = true; - valid = valid && (await this.ezsp.networkInit()); - const netParams = await this.ezsp.execCommand('getNetworkParameters'); - const networkParams = netParams.parameters; - logger.debug(`Current Node type: ${netParams.nodeType}, Network parameters: ${networkParams}`, NS); - valid = valid && netParams.status == EmberStatus.SUCCESS; - valid = valid && netParams.nodeType == EmberNodeType.COORDINATOR; - valid = valid && options.panID == networkParams.panId; - valid = valid && options.channelList.includes(networkParams.radioChannel); - valid = valid && equals(options.extendedPanID, networkParams.extendedPanId); - return !valid; - } - - private async formNetwork(restore: boolean, transmitPower?: number): Promise { - let backup; - await this.ezsp.execCommand('clearTransientLinkKeys'); - - let initial_security_state: EmberInitialSecurityState; - if (restore) { - backup = await this.backupMan.getStoredBackup(); - - if (!backup) { - throw new Error(`No valid backup found.`); - } - - initial_security_state = ember_security(backup.networkOptions.networkKey); - initial_security_state.bitmask |= EmberInitialSecurityBitmask.NO_FRAME_COUNTER_RESET; - initial_security_state.networkKeySequenceNumber = backup.networkKeyInfo.sequenceNumber; - initial_security_state.preconfiguredKey.contents = backup.ezsp!.hashed_tclk!; - } else { - await this.ezsp.execCommand('clearKeyTable'); - initial_security_state = ember_security(Buffer.from(this.nwkOpt.networkKey!)); - } - await this.ezsp.setInitialSecurityState(initial_security_state); - - const parameters: EmberNetworkParameters = new EmberNetworkParameters(); - parameters.radioTxPower = transmitPower ?? 5; - parameters.joinMethod = EmberJoinMethod.USE_MAC_ASSOCIATION; - parameters.nwkManagerId = 0; - parameters.nwkUpdateId = 0; - parameters.channels = 0x07fff800; // all channels - if (restore) { - // `backup` valid from above - parameters.panId = backup!.networkOptions.panId; - parameters.extendedPanId = backup!.networkOptions.extendedPanId; - parameters.radioChannel = backup!.logicalChannel; - parameters.nwkUpdateId = backup!.networkUpdateId; - } else { - parameters.radioChannel = this.nwkOpt.channelList[0]; - parameters.panId = this.nwkOpt.panID; - parameters.extendedPanId = Buffer.from(this.nwkOpt.extendedPanID!); - } - - await this.ezsp.formNetwork(parameters); - await this.ezsp.setValue(EzspValueId.VALUE_STACK_TOKEN_WRITING, 1); - } - private handleFrame(frameName: string, frame: EZSPFrameData): void { switch (true) { case frameName === 'incomingMessageHandler': { @@ -937,59 +830,4 @@ export class Driver extends EventEmitter { return keyInfo; } } - - private async needsToBeRestore(options: TsType.NetworkOptions): Promise { - // if no backup and the settings have been changed, then need to start a new network - const backup = await this.backupMan.getStoredBackup(); - if (!backup) return false; - - let valid = true; - //valid = valid && (await this.ezsp.networkInit()); - const netParams = await this.ezsp.execCommand('getNetworkParameters'); - const networkParams = netParams.parameters; - logger.debug(`Current Node type: ${netParams.nodeType}, Network parameters: ${networkParams}`, NS); - logger.debug(`Backuped network parameters: ${backup.networkOptions}`, NS); - const networkKey = await this.getKey(EmberKeyType.CURRENT_NETWORK_KEY); - let netKey: Buffer; - - if (this.ezsp.ezspV < 13) { - netKey = Buffer.from((networkKey.keyStruct as EmberKeyStruct).key.contents); - } else { - netKey = Buffer.from((networkKey.keyData as EmberKeyData).contents); - } - - // if the settings in the backup match the chip, then need to warn to delete the backup file first - valid = valid && networkParams.panId == backup.networkOptions.panId; - valid = valid && networkParams.radioChannel == backup.logicalChannel; - valid = valid && Buffer.from(networkParams.extendedPanId).equals(backup.networkOptions.extendedPanId); - valid = valid && Buffer.from(netKey).equals(backup.networkOptions.networkKey); - if (valid) { - logger.error(`Configuration is not consistent with adapter backup!`, NS); - logger.error(`- PAN ID: configured=${options.panID}, adapter=${networkParams.panId}, backup=${backup.networkOptions.panId}`, NS); - logger.error( - `- Extended PAN ID: configured=${Buffer.from(options.extendedPanID!).toString('hex')}, ` + - `adapter=${Buffer.from(networkParams.extendedPanId).toString('hex')}, ` + - `backup=${Buffer.from(networkParams.extendedPanId).toString('hex')}`, - NS, - ); - logger.error(`- Channel: configured=${options.channelList}, adapter=${networkParams.radioChannel}, backup=${backup.logicalChannel}`, NS); - logger.error( - `- Network key: configured=${Buffer.from(options.networkKey!).toString('hex')}, ` + - `adapter=${Buffer.from(netKey).toString('hex')}, ` + - `backup=${backup.networkOptions.networkKey.toString('hex')}`, - NS, - ); - logger.error(`Please update configuration to prevent further issues.`, NS); - logger.error(`If you wish to re-commission your network, please remove coordinator backup.`, NS); - logger.error(`Re-commissioning your network will require re-pairing of all devices!`, NS); - throw new Error('startup failed - configuration-adapter mismatch - see logs above for more information'); - } - valid = true; - // if the settings in the backup match the config, then the old network is in the chip and needs to be restored - valid = valid && options.panID == backup.networkOptions.panId; - valid = valid && options.channelList.includes(backup.logicalChannel); - valid = valid && Buffer.from(options.extendedPanID!).equals(backup.networkOptions.extendedPanId); - valid = valid && Buffer.from(options.networkKey!).equals(backup.networkOptions.networkKey); - return valid; - } } diff --git a/src/adapter/ezsp/driver/ezsp.ts b/src/adapter/ezsp/driver/ezsp.ts index cb5e0adea1..48005bd91e 100644 --- a/src/adapter/ezsp/driver/ezsp.ts +++ b/src/adapter/ezsp/driver/ezsp.ts @@ -477,8 +477,7 @@ export class Ezsp extends EventEmitter { if (result.status !== EmberStatus.SUCCESS) { this.waitress.remove(waiter.ID); - logger.error('Failure to init network', NS); - return false; + throw new Error('Failure to init network'); } const response = await waiter.start().promise; @@ -494,16 +493,13 @@ export class Ezsp extends EventEmitter { if (result.status !== EmberStatus.SUCCESS) { this.waitress.remove(waiter.ID); - logger.debug('Failure to leave network', NS); throw new Error('Failure to leave network: ' + JSON.stringify(result)); } const response = await waiter.start().promise; if (response.payload.status !== EmberStatus.NETWORK_DOWN) { - const msg = `Wrong network status: ${JSON.stringify(response.payload)}`; - logger.debug(msg, NS); - throw new Error(msg); + throw new Error(`Wrong network status: ${JSON.stringify(response.payload)}`); } return response.payload.status; diff --git a/src/adapter/ezsp/driver/types/struct.ts b/src/adapter/ezsp/driver/types/struct.ts index cf4095be5e..2c54107eb3 100644 --- a/src/adapter/ezsp/driver/types/struct.ts +++ b/src/adapter/ezsp/driver/types/struct.ts @@ -33,22 +33,14 @@ export class EzspStruct { } export class EmberNetworkParameters extends EzspStruct { - // @ts-expect-error set via _fields - public extendedPanId: Buffer; - // @ts-expect-error set via _fields - public panId: number; - // @ts-expect-error set via _fields - public radioTxPower: number; - // @ts-expect-error set via _fields - public radioChannel: number; - // @ts-expect-error set via _fields - public joinMethod: named.EmberJoinMethod; - // @ts-expect-error set via _fields - public nwkManagerId: named.EmberNodeId; - // @ts-expect-error set via _fields - public nwkUpdateId: number; - // @ts-expect-error set via _fields - public channels: number; + public extendedPanId!: Buffer; + public panId!: number; + public radioTxPower!: number; + public radioChannel!: number; + public joinMethod!: named.EmberJoinMethod; + public nwkManagerId!: named.EmberNodeId; + public nwkUpdateId!: number; + public channels!: number; static _fields = [ // The network's extended PAN identifier. @@ -97,16 +89,11 @@ export class EmberZigbeeNetwork extends EzspStruct { } export class EmberApsFrame extends EzspStruct { - // @ts-expect-error set via _fields - public profileId: number; - // @ts-expect-error set via _fields - public sequence: number; - // @ts-expect-error set via _fields - public clusterId: number; - // @ts-expect-error set via _fields - public sourceEndpoint: number; - // @ts-expect-error set via _fields - public destinationEndpoint: number; + public profileId!: number; + public sequence!: number; + public clusterId!: number; + public sourceEndpoint!: number; + public destinationEndpoint!: number; public groupId?: number; public options?: named.EmberApsOption; @@ -154,12 +141,9 @@ export class EmberBindingTableEntry extends EzspStruct { } export class EmberMulticastTableEntry extends EzspStruct { - // @ts-expect-error set via _fields - public multicastId: number; - // @ts-expect-error set via _fields - public endpoint: number; - // @ts-expect-error set via _fields - public networkIndex: number; + public multicastId!: number; + public endpoint!: number; + public networkIndex!: number; // A multicast table entry indicates that a particular endpoint is a member // of a particular multicast group.Only devices with an endpoint in a // multicast group will receive messages sent to that multicast group. @@ -175,8 +159,7 @@ export class EmberMulticastTableEntry extends EzspStruct { } export class EmberKeyData extends EzspStruct { - // @ts-expect-error set via _fields - public contents: Buffer; + public contents!: Buffer; // A 128- bit key. static _fields = [ // The key data. @@ -185,8 +168,7 @@ export class EmberKeyData extends EzspStruct { } export class EmberCertificateData extends EzspStruct { - // @ts-expect-error set via _fields - public contents: Buffer; + public contents!: Buffer; // The implicit certificate used in CBKE. static _fields = [ // The certificate data. @@ -195,8 +177,7 @@ export class EmberCertificateData extends EzspStruct { } export class EmberPublicKeyData extends EzspStruct { - // @ts-expect-error set via _fields - public contents: Buffer; + public contents!: Buffer; // The public key data used in CBKE. static _fields = [ // The public key data. @@ -205,8 +186,7 @@ export class EmberPublicKeyData extends EzspStruct { } export class EmberPrivateKeyData extends EzspStruct { - // @ts-expect-error set via _fields - public contents: Buffer; + public contents!: Buffer; // The private key data used in CBKE. static _fields = [ // The private key data. @@ -271,10 +251,8 @@ export class EmberMessageDigest extends EzspStruct { } export class EmberAesMmoHashContext extends EzspStruct { - // @ts-expect-error set via _fields - public result: Buffer; - // @ts-expect-error set via _fields - public length: number; + public result!: Buffer; + public length!: number; // The hash context for an ongoing hash operation. static _fields = [ // The result of ongoing the hash operation. @@ -336,16 +314,11 @@ export class EmberRouteTableEntry extends EzspStruct { } export class EmberInitialSecurityState extends EzspStruct { - // @ts-expect-error set via _fields - public bitmask: number; - // @ts-expect-error set via _fields - public preconfiguredKey: EmberKeyData; - // @ts-expect-error set via _fields - public networkKey: EmberKeyData; - // @ts-expect-error set via _fields - public networkKeySequenceNumber: number; - // @ts-expect-error set via _fields - public preconfiguredTrustCenterEui64: named.EmberEUI64; + public bitmask!: number; + public preconfiguredKey!: EmberKeyData; + public networkKey!: EmberKeyData; + public networkKeySequenceNumber!: number; + public preconfiguredTrustCenterEui64!: named.EmberEUI64; // The security data used to set the configuration for the stack, or the // retrieved configuration currently in use. @@ -390,12 +363,9 @@ export class EmberCurrentSecurityState extends EzspStruct { } export class EmberKeyStruct extends EzspStruct { - // @ts-expect-error set via _fields - public key: EmberKeyData; - // @ts-expect-error set via _fields - public outgoingFrameCounter: number; - // @ts-expect-error set via _fields - public sequenceNumber: number; + public key!: EmberKeyData; + public outgoingFrameCounter!: number; + public sequenceNumber!: number; // A structure containing a key and its associated data. static _fields = [ // A bitmask indicating the presence of data within the various fields @@ -723,26 +693,16 @@ export class EmberRoutingTable extends EzspStruct { } export class EmberRawFrame extends EzspStruct { - // @ts-expect-error set via _fields - public ieeeFrameControl: number; - // @ts-expect-error set via _fields - public sequence: number; - // @ts-expect-error set via _fields - public destPanId: number; - // @ts-expect-error set via _fields - public destNodeId: named.EmberNodeId; - // @ts-expect-error set via _fields - public sourcePanId: number; - // @ts-expect-error set via _fields - public ieeeAddress: named.EmberEUI64; - // @ts-expect-error set via _fields - public nwkFrameControl: number; - // @ts-expect-error set via _fields - public appFrameControl: number; - // @ts-expect-error set via _fields - public clusterId: number; - // @ts-expect-error set via _fields - public profileId: number; + public ieeeFrameControl!: number; + public sequence!: number; + public destPanId!: number; + public destNodeId!: named.EmberNodeId; + public sourcePanId!: number; + public ieeeAddress!: named.EmberEUI64; + public nwkFrameControl!: number; + public appFrameControl!: number; + public clusterId!: number; + public profileId!: number; static _fields = [ ['ieeeFrameControl', basic.uint16_t], @@ -759,26 +719,16 @@ export class EmberRawFrame extends EzspStruct { } export class EmberIeeeRawFrame extends EzspStruct { - // @ts-expect-error set via _fields - public ieeeFrameControl: number; - // @ts-expect-error set via _fields - public sequence: number; - // @ts-expect-error set via _fields - public destPanId: number; - // @ts-expect-error set via _fields - public destAddress: named.EmberEUI64; - // @ts-expect-error set via _fields - public sourcePanId: number; - // @ts-expect-error set via _fields - public sourceAddress: named.EmberEUI64; - // @ts-expect-error set via _fields - public nwkFrameControl: number; - // @ts-expect-error set via _fields - public appFrameControl: number; - // @ts-expect-error set via _fields - public clusterId: number; - // @ts-expect-error set via _fields - public profileId: number; + public ieeeFrameControl!: number; + public sequence!: number; + public destPanId!: number; + public destAddress!: named.EmberEUI64; + public sourcePanId!: number; + public sourceAddress!: named.EmberEUI64; + public nwkFrameControl!: number; + public appFrameControl!: number; + public clusterId!: number; + public profileId!: number; static _fields = [ ['ieeeFrameControl', basic.uint16_t], ['sequence', basic.uint8_t], @@ -795,20 +745,13 @@ export class EmberIeeeRawFrame extends EzspStruct { export class EmberSecurityManagerContext extends EzspStruct { // Context for Zigbee Security Manager operations. - // @ts-expect-error set via _fields - public type: named.EmberKeyType; - // @ts-expect-error set via _fields - public index: number; - // @ts-expect-error set via _fields - public derivedType: named.EmberDerivedKeyType; - // @ts-expect-error set via _fields - public eui64: named.EmberEUI64; - // @ts-expect-error set via _fields - public multiNetworkIndex: number; - // @ts-expect-error set via _fields - public flags: number; - // @ts-expect-error set via _fields - public psaKeyAlgPermission: basic.uint32_t; + public type!: named.EmberKeyType; + public index!: number; + public derivedType!: named.EmberDerivedKeyType; + public eui64!: named.EmberEUI64; + public multiNetworkIndex!: number; + public flags!: number; + public psaKeyAlgPermission!: basic.uint32_t; static _fields = [ // The type of key being referenced. ['type', named.EmberKeyType], @@ -829,16 +772,11 @@ export class EmberSecurityManagerContext extends EzspStruct { /** This data structure contains the metadata pertaining to an network key */ export class EmberSecurityManagerNetworkKeyInfo extends EzspStruct { - // @ts-expect-error set via _fields - public networkKeySet: number; // boolean - // @ts-expect-error set via _fields - public alternateNetworkKeySet: number; // boolean - // @ts-expect-error set via _fields - public networkKeySequenceNumber: number; - // @ts-expect-error set via _fields - public altNetworkKeySequenceNumber: number; - // @ts-expect-error set via _fields - public networkKeyFrameCounter: number; + public networkKeySet!: number; // boolean + public alternateNetworkKeySet!: number; // boolean + public networkKeySequenceNumber!: number; + public altNetworkKeySequenceNumber!: number; + public networkKeyFrameCounter!: number; static _fields = [ ['networkKeySet', basic.uint8_t], ['alternateNetworkKeySet', basic.uint8_t], From e1bfece0d348c6e0b18ec18fc7c51bf32f903b8f Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:58:48 +0100 Subject: [PATCH 08/16] prelim zboss --- src/adapter/zboss/adapter/zbossAdapter.ts | 141 ++++++++++++++++++++-- src/adapter/zboss/driver.ts | 128 ++------------------ 2 files changed, 140 insertions(+), 129 deletions(-) diff --git a/src/adapter/zboss/adapter/zbossAdapter.ts b/src/adapter/zboss/adapter/zbossAdapter.ts index 664cd67b64..1b3bd64834 100644 --- a/src/adapter/zboss/adapter/zbossAdapter.ts +++ b/src/adapter/zboss/adapter/zbossAdapter.ts @@ -3,6 +3,7 @@ import assert from 'assert'; import {Adapter, TsType} from '../..'; +import * as Models from '../../../models'; import {Backup} from '../../../models'; import {Queue, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; @@ -12,7 +13,7 @@ import * as Zdo from '../../../zspec/zdo'; import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; import {ZclPayload} from '../../events'; import {ZBOSSDriver} from '../driver'; -import {CommandId, DeviceUpdateStatus} from '../enums'; +import {CommandId, DeviceType, DeviceUpdateStatus, PolicyType, ResetOptions} from '../enums'; import {FrameType, ZBOSSFrame} from '../frame'; const NS = 'zh:zboss'; @@ -25,10 +26,22 @@ interface WaitressMatcher { commandIdentifier: number; } +export type ZBOSSNetworkInfo = { + joined: boolean; + nodeType: DeviceType; + ieeeAddr: string; + network: { + panID: number; + extendedPanID: number[]; + channel: number; + }; +}; + export class ZBOSSAdapter extends Adapter { private queue: Queue; private readonly driver: ZBOSSDriver; private waitress: Waitress; + public netInfo!: ZBOSSNetworkInfo; // expected valid upon startup of driver constructor( networkOptions: TsType.NetworkOptions, @@ -44,7 +57,7 @@ export class ZBOSSAdapter extends Adapter { this.queue = new Queue(concurrent); this.waitress = new Waitress(this.waitressValidator, this.waitressTimeoutFormatter); - this.driver = new ZBOSSDriver(serialPortOptions, networkOptions); + this.driver = new ZBOSSDriver(serialPortOptions); this.driver.on('frame', this.processMessage.bind(this)); } @@ -107,7 +120,46 @@ export class ZBOSSAdapter extends Adapter { await this.driver.connect(); - return await this.driver.startup(this.adapterOptions.transmitPower); + const result = await this.initNetwork(); + + if (result === 'resumed') { + await this.driver.execCommand(CommandId.NWK_START_WITHOUT_FORMATION, {}); + } + + await this.driver.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.LINK_KEY_REQUIRED, value: 0}); + await this.driver.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.IC_REQUIRED, value: 0}); + await this.driver.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.TC_REJOIN_ENABLED, value: 1}); + await this.driver.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.IGNORE_TC_REJOIN, value: 0}); + await this.driver.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.APS_INSECURE_JOIN, value: 0}); + await this.driver.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.DISABLE_NWK_MGMT_CHANNEL_UPDATE, value: 0}); + + // TODO: use ZSpec.XYZ and Zcl.Cluster.xyz.ID + await this.driver.addEndpoint( + 1, + 260, + 0xbeef, + [0x0000, 0x0003, 0x0006, 0x000a, 0x0019, 0x001a, 0x0300], + [ + 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0020, 0x0300, 0x0400, 0x0402, 0x0405, 0x0406, 0x0500, 0x0b01, 0x0b03, 0x0b04, + 0x0702, 0x1000, 0xfc01, 0xfc02, + ], + ); + await this.driver.addEndpoint(242, 0xa1e0, 0x61, [], [0x0021]); + + // update cache after finished with network init stuff + this.netInfo = await this.driver.getNetworkInfo(); + + logger.debug(() => `Network Ready! ${JSON.stringify(this.netInfo)}`, NS); + + await this.driver.execCommand(CommandId.SET_RX_ON_WHEN_IDLE, {rxOn: 1}); + //await this.driver.execCommand(CommandId.SET_ED_TIMEOUT, {timeout: 8}); + //await this.driver.execCommand(CommandId.SET_MAX_CHILDREN, {children: 100}); + + if (this.adapterOptions.transmitPower != undefined) { + await this.driver.execCommand(CommandId.SET_TX_POWER, {txPower: this.adapterOptions.transmitPower}); + } + + return result; } public async stop(): Promise { @@ -116,8 +168,74 @@ export class ZBOSSAdapter extends Adapter { logger.info(`ZBOSS Adapter stopped`, NS); } + /** + * Check the network status on the adapter (execute the necessary pre-steps to be able to get it). + * WARNING: This is a one-off. Should not be called outside of `initNetwork`. + */ + protected async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { + this.netInfo = await this.driver.getNetworkInfo(); + + if (this.netInfo?.joined) { + // force forming in case current network is `ROUTER` type + if (this.netInfo.nodeType !== DeviceType.COORDINATOR) { + return [false, undefined, undefined]; + } + + logger.debug(() => `Current network parameters: ${JSON.stringify(this.netInfo)}`, NS); + + return [true, this.netInfo.network.panID, Buffer.from(this.netInfo.network.extendedPanID)]; + } + + return [false, undefined, undefined]; + } + + public async leaveNetwork(): Promise { + // TODO: might have other side-effects than just leaving network? + await this.driver.reset(ResetOptions.FactoryReset); + } + + /** + * If backup is defined, form network from backup, otherwise from config. + */ + public async formNetwork(backup?: Models.Backup): Promise { + if (backup) { + // this path should never be reached + throw new Error('This adapter does not support backup'); + } else { + const channelMask = ZSpec.Utils.channelsToUInt32Mask(this.networkOptions.channelList); + + await this.driver.execCommand(CommandId.SET_ZIGBEE_ROLE, {role: DeviceType.COORDINATOR}); + await this.driver.execCommand(CommandId.SET_ZIGBEE_CHANNEL_MASK, {page: 0, mask: channelMask}); + await this.driver.execCommand(CommandId.SET_PAN_ID, {panID: this.networkOptions.panID}); + // await this.driver.execCommand(CommandId.SET_EXTENDED_PAN_ID, {extendedPanID: this.networkOptions.extendedPanID}); + await this.driver.execCommand(CommandId.SET_NWK_KEY, {nwkKey: this.networkOptions.networkKey, index: 0}); + + const res = await this.driver.execCommand( + CommandId.NWK_FORMATION, + { + len: 1, + channels: [{page: 0, mask: channelMask}], + duration: 0x05, + distribFlag: 0x00, + distribNwk: 0x0000, + extendedPanID: this.networkOptions.extendedPanID, + }, + 20000, + ); + + logger.debug(() => `Forming network: ${JSON.stringify(res)}`, NS); + } + } + + public async getNetworkKey(): Promise { + const res = await this.driver.execCommand(CommandId.GET_NWK_KEYS); + + // XXX: assumed typed from CommandId.SET_NWK_KEY above + return Buffer.from(res.payload.nwkKey1 as number[]); + } + public async getCoordinatorIEEE(): Promise { - return this.driver.netInfo.ieeeAddr; + return this.netInfo.ieeeAddr; } public async getCoordinatorVersion(): Promise { @@ -152,6 +270,12 @@ export class ZBOSSAdapter extends Adapter { return false; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public checkBackup(backup: Models.Backup): void { + // always fail if found a backup, since not supported, it can't be for zboss + throw new Error('This adapter does not support backup'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars public async backup(ieeeAddressesInDatabase: string[]): Promise { throw new Error('This adapter does not support backup'); @@ -159,9 +283,10 @@ export class ZBOSSAdapter extends Adapter { public async getNetworkParameters(): Promise { return await this.queue.execute(async () => { - const channel = this.driver.netInfo!.network.channel; - const panID = this.driver.netInfo!.network.panID!; - const extendedPanID = this.driver.netInfo!.network.extendedPanID; + // TODO: this will not be up-to-date if channel changed by controller on start, need cache refresh mechanism + const channel = this.netInfo.network.channel; + const panID = this.netInfo.network.panID; + const extendedPanID = this.netInfo.network.extendedPanID; return { panID, @@ -320,7 +445,7 @@ export class ZBOSSAdapter extends Adapter { assocRestore: {ieeeadr: string; nwkaddr: number; noderelation: number} | null, ): Promise { if (ieeeAddr == null) { - ieeeAddr = this.driver.netInfo.ieeeAddr; + ieeeAddr = this.netInfo.ieeeAddr; } logger.debug( `sendZclFrameToEndpointInternal ${ieeeAddr}:${networkAddress}/${endpoint} ` + diff --git a/src/adapter/zboss/driver.ts b/src/adapter/zboss/driver.ts index 727a030ed8..9ae57361ce 100644 --- a/src/adapter/zboss/driver.ts +++ b/src/adapter/zboss/driver.ts @@ -3,16 +3,15 @@ import assert from 'assert'; import EventEmitter from 'events'; -import equals from 'fast-deep-equal/es6'; - import {TsType} from '..'; import {KeyValue} from '../../controller/tstype'; import {Queue, Waitress} from '../../utils'; import {logger} from '../../utils/logger'; import * as ZSpec from '../../zspec'; import * as Zdo from '../../zspec/zdo'; +import {ZBOSSNetworkInfo} from './adapter/zbossAdapter'; import {ZDO_REQ_CLUSTER_ID_TO_ZBOSS_COMMAND_ID} from './commands'; -import {CommandId, DeviceType, PolicyType, ResetOptions, StatusCodeGeneric} from './enums'; +import {CommandId, ResetOptions, StatusCodeGeneric} from './enums'; import {FrameType, makeFrame, ZBOSSFrame} from './frame'; import {ZBOSSUart} from './uart'; @@ -25,28 +24,14 @@ type ZBOSSWaitressMatcher = { commandId: number; }; -type ZBOSSNetworkInfo = { - joined: boolean; - nodeType: DeviceType; - ieeeAddr: string; - network: { - panID: number; - extendedPanID: number[]; - channel: number; - }; -}; - export class ZBOSSDriver extends EventEmitter { public readonly port: ZBOSSUart; private waitress: Waitress; private queue: Queue; private tsn = 1; // command sequence - private nwkOpt: TsType.NetworkOptions; - public netInfo!: ZBOSSNetworkInfo; // expected valid upon startup of driver - constructor(options: TsType.SerialPortOptions, nwkOpt: TsType.NetworkOptions) { + constructor(options: TsType.SerialPortOptions) { super(); - this.nwkOpt = nwkOpt; this.queue = new Queue(); this.waitress = new Waitress(this.waitressValidator, this.waitressTimeoutFormatter); @@ -78,88 +63,16 @@ export class ZBOSSDriver extends EventEmitter { return status; } - private async reset(options = ResetOptions.NoOptions): Promise { + public async reset(options = ResetOptions.NoOptions): Promise { logger.info(`Driver reset`, NS); this.port.inReset = true; await this.execCommand(CommandId.NCP_RESET, {options}, 10000); } - public async startup(transmitPower?: number): Promise { - logger.info(`Driver startup`, NS); - let result: TsType.StartResult = 'resumed'; - - if (await this.needsToBeInitialised(this.nwkOpt)) { - // need to check the backup - // const restore = await this.needsToBeRestore(this.nwkOpt); - const restore = false; - - if (this.netInfo.joined) { - logger.info(`Leaving current network and forming new network`, NS); - await this.reset(ResetOptions.FactoryReset); - } - - if (restore) { - // // restore - // logger.info('Restore network from backup', NS); - // await this.formNetwork(true); - // result = 'restored'; - } else { - // reset - logger.info('Form network', NS); - await this.formNetwork(); // false - result = 'reset'; - } - } else { - await this.execCommand(CommandId.NWK_START_WITHOUT_FORMATION, {}); - } - await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.LINK_KEY_REQUIRED, value: 0}); - await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.IC_REQUIRED, value: 0}); - await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.TC_REJOIN_ENABLED, value: 1}); - await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.IGNORE_TC_REJOIN, value: 0}); - await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.APS_INSECURE_JOIN, value: 0}); - await this.execCommand(CommandId.SET_TC_POLICY, {type: PolicyType.DISABLE_NWK_MGMT_CHANNEL_UPDATE, value: 0}); - - await this.addEndpoint( - 1, - 260, - 0xbeef, - [0x0000, 0x0003, 0x0006, 0x000a, 0x0019, 0x001a, 0x0300], - [ - 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0020, 0x0300, 0x0400, 0x0402, 0x0405, 0x0406, 0x0500, 0x0b01, 0x0b03, 0x0b04, - 0x0702, 0x1000, 0xfc01, 0xfc02, - ], - ); - await this.addEndpoint(242, 0xa1e0, 0x61, [], [0x0021]); - - await this.execCommand(CommandId.SET_RX_ON_WHEN_IDLE, {rxOn: 1}); - //await this.execCommand(CommandId.SET_ED_TIMEOUT, {timeout: 8}); - //await this.execCommand(CommandId.SET_MAX_CHILDREN, {children: 100}); - - if (transmitPower != undefined) { - await this.execCommand(CommandId.SET_TX_POWER, {txPower: transmitPower}); - } - - return result; - } - - private async needsToBeInitialised(options: TsType.NetworkOptions): Promise { - let valid = true; - this.netInfo = await this.getNetworkInfo(); - logger.debug(() => `Current network parameters: ${JSON.stringify(this.netInfo)}`, NS); - if (this.netInfo) { - valid = valid && this.netInfo.nodeType == DeviceType.COORDINATOR; - valid = valid && options.panID == this.netInfo.network.panID; - valid = valid && options.channelList.includes(this.netInfo.network.channel); - valid = valid && equals(Buffer.from(options.extendedPanID || []), Buffer.from(this.netInfo.network.extendedPanID)); - } else { - valid = false; - } - return !valid; - } - - private async getNetworkInfo(): Promise { + public async getNetworkInfo(): Promise { let result = await this.execCommand(CommandId.GET_JOINED, {}); const joined = result.payload.joined == 1; + if (!joined) { logger.debug('Network not formed', NS); } @@ -191,7 +104,7 @@ export class ZBOSSDriver extends EventEmitter { }; } - private async addEndpoint( + public async addEndpoint( endpoint: number, profileId: number, deviceId: number, @@ -212,33 +125,6 @@ export class ZBOSSDriver extends EventEmitter { logger.debug(() => `Adding endpoint: ${JSON.stringify(res)}`, NS); } - private getChannelMask(channels: number[]): number { - return channels.reduce((mask, channel) => mask | (1 << channel), 0); - } - - private async formNetwork(): Promise { - const channelMask = this.getChannelMask(this.nwkOpt.channelList); - await this.execCommand(CommandId.SET_ZIGBEE_ROLE, {role: DeviceType.COORDINATOR}); - await this.execCommand(CommandId.SET_ZIGBEE_CHANNEL_MASK, {page: 0, mask: channelMask}); - await this.execCommand(CommandId.SET_PAN_ID, {panID: this.nwkOpt.panID}); - // await this.execCommand(CommandId.SET_EXTENDED_PAN_ID, {extendedPanID: this.nwkOpt.extendedPanID}); - await this.execCommand(CommandId.SET_NWK_KEY, {nwkKey: this.nwkOpt.networkKey, index: 0}); - - const res = await this.execCommand( - CommandId.NWK_FORMATION, - { - len: 1, - channels: [{page: 0, mask: channelMask}], - duration: 0x05, - distribFlag: 0x00, - distribNwk: 0x0000, - extendedPanID: this.nwkOpt.extendedPanID, - }, - 20000, - ); - logger.debug(() => `Forming network: ${JSON.stringify(res)}`, NS); - } - public async stop(): Promise { await this.port.stop(); From 204c378e547979f8119445398045eb2e4f6c95e5 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sat, 14 Dec 2024 20:51:10 +0100 Subject: [PATCH 09/16] prelim zstack --- src/adapter/adapter.ts | 40 ++- src/adapter/z-stack/adapter/adapter-backup.ts | 33 +- src/adapter/z-stack/adapter/manager.ts | 281 ++++-------------- src/adapter/z-stack/adapter/zStackAdapter.ts | 62 +++- src/adapter/z-stack/models/startup-options.ts | 1 - 5 files changed, 136 insertions(+), 281 deletions(-) diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index f010c5e636..3da56df4b4 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -10,7 +10,7 @@ import * as Zdo from '../zspec/zdo'; import * as ZdoTypes from '../zspec/zdo/definition/tstypes'; import {discoverAdapter} from './adapterDiscovery'; import * as AdapterEvents from './events'; -import * as TsType from './tstype'; +import {AdapterOptions, CoordinatorVersion, NetworkOptions, NetworkParameters, SerialPortOptions, StartResult} from './tstype'; interface AdapterEventMap { deviceJoined: [payload: AdapterEvents.DeviceJoinedPayload]; @@ -23,17 +23,12 @@ interface AdapterEventMap { abstract class Adapter extends events.EventEmitter { public hasZdoMessageOverhead: boolean; public manufacturerID: Zcl.ManufacturerCode; - protected networkOptions: TsType.NetworkOptions; - protected adapterOptions: TsType.AdapterOptions; - protected serialPortOptions: TsType.SerialPortOptions; + protected networkOptions: NetworkOptions; + protected adapterOptions: AdapterOptions; + protected serialPortOptions: SerialPortOptions; protected backupPath: string; - protected constructor( - networkOptions: TsType.NetworkOptions, - serialPortOptions: TsType.SerialPortOptions, - backupPath: string, - adapterOptions: TsType.AdapterOptions, - ) { + protected constructor(networkOptions: NetworkOptions, serialPortOptions: SerialPortOptions, backupPath: string, adapterOptions: AdapterOptions) { super(); this.hasZdoMessageOverhead = true; this.manufacturerID = Zcl.ManufacturerCode.RESERVED_10; @@ -48,10 +43,10 @@ abstract class Adapter extends events.EventEmitter { */ public static async create( - networkOptions: TsType.NetworkOptions, - serialPortOptions: TsType.SerialPortOptions, + networkOptions: NetworkOptions, + serialPortOptions: SerialPortOptions, backupPath: string, - adapterOptions: TsType.AdapterOptions, + adapterOptions: AdapterOptions, ): Promise { const {ZStackAdapter} = await import('./z-stack/adapter'); const {DeconzAdapter} = await import('./deconz/adapter'); @@ -86,7 +81,7 @@ abstract class Adapter extends events.EventEmitter { * - reset: network in configuration.yaml does not match network in adapter, no valid backup, form a new network * - restored: network in configuration.yaml does not match network in adapter, valid backup, restore from backup */ - public async initNetwork(): Promise { + public async initNetwork(): Promise { const enum InitAction { DONE, /** Config mismatch, must leave network. */ @@ -155,14 +150,15 @@ abstract class Adapter extends events.EventEmitter { switch (action) { case InitAction.FORM_BACKUP: { assert(backup); - await this.formNetwork(backup); - return 'restored'; + const formResult = await this.formNetwork(backup); + + return formResult || 'restored'; } case InitAction.FORM_CONFIG: { - await this.formNetwork(); + const formResult = await this.formNetwork(); - return 'reset'; + return formResult || 'reset'; } case InitAction.DONE: { return 'resumed'; @@ -170,7 +166,7 @@ abstract class Adapter extends events.EventEmitter { } } - public abstract start(): Promise; + public abstract start(): Promise; public abstract stop(): Promise; @@ -185,13 +181,13 @@ abstract class Adapter extends events.EventEmitter { /** * If backup is defined, form network from backup, otherwise from config. */ - public abstract formNetwork(backup?: Models.Backup): Promise; + public abstract formNetwork(backup?: Models.Backup): Promise; public abstract getNetworkKey(): Promise; public abstract getCoordinatorIEEE(): Promise; - public abstract getCoordinatorVersion(): Promise; + public abstract getCoordinatorVersion(): Promise; public abstract reset(type: 'soft' | 'hard'): Promise; @@ -238,7 +234,7 @@ abstract class Adapter extends events.EventEmitter { public abstract backup(ieeeAddressesInDatabase: string[]): Promise; - public abstract getNetworkParameters(): Promise; + public abstract getNetworkParameters(): Promise; public abstract addInstallCode(ieeeAddress: string, key: Buffer): Promise; diff --git a/src/adapter/z-stack/adapter/adapter-backup.ts b/src/adapter/z-stack/adapter/adapter-backup.ts index 1d3f431ddb..edfe85d277 100644 --- a/src/adapter/z-stack/adapter/adapter-backup.ts +++ b/src/adapter/z-stack/adapter/adapter-backup.ts @@ -1,8 +1,6 @@ import assert from 'assert'; -import * as fs from 'fs'; import * as Models from '../../../models'; -import {BackupUtils} from '../../../utils'; import {logger} from '../../../utils/logger'; import {NULL_NODE_ID, Utils as ZSpecUtils} from '../../../zspec'; import {NvItemsIds, NvSystemIds} from '../constants/common'; @@ -30,37 +28,10 @@ export class AdapterBackup { this.defaultPath = path; } - /** - * Loads currently stored backup and returns it in internal backup model. - */ - public async getStoredBackup(): Promise { - try { - fs.accessSync(this.defaultPath); - } catch { - return undefined; - } - let data; - try { - data = JSON.parse(fs.readFileSync(this.defaultPath).toString()); - } catch (error) { - throw new Error(`Coordinator backup is corrupted (${error})`); - } - if (data.metadata?.format === 'zigpy/open-coordinator-backup' && data.metadata?.version) { - if (data.metadata?.version !== 1) { - throw new Error(`Unsupported open coordinator backup version (version=${data.metadata?.version})`); - } - return BackupUtils.fromUnifiedBackup(data as Models.UnifiedBackupStorage); - } else if (data.adapterType === 'zStack') { - return BackupUtils.fromLegacyBackup(data as Models.LegacyBackupStorage); - } else { - throw new Error('Unknown backup format'); - } - } - /** * Creates a new backup from connected ZNP adapter and returns it in internal backup model format. */ - public async createBackup(ieeeAddressesInDatabase: string[]): Promise { + public async createBackup(ieeeAddressesInDatabase: string[], getExistingBackup: () => Models.Backup | undefined): Promise { logger.debug('creating backup', NS); const version: ZnpVersion = await this.getAdapterVersion(); @@ -227,7 +198,7 @@ export class AdapterBackup { * does not have the linkKey anymore. * Below we don't remove any devices from the backup which have a linkkey and are still in the database (=ieeeAddressesInDatabase) */ - const oldBackup = await this.getStoredBackup(); + const oldBackup = getExistingBackup(); assert(oldBackup, "Old backup doesn't exist"); const missing = oldBackup.devices.filter( (d) => diff --git a/src/adapter/z-stack/adapter/manager.ts b/src/adapter/z-stack/adapter/manager.ts index fdad9b1816..aa8d679986 100644 --- a/src/adapter/z-stack/adapter/manager.ts +++ b/src/adapter/z-stack/adapter/manager.ts @@ -6,6 +6,7 @@ import {Wait} from '../../../utils'; import {logger} from '../../../utils/logger'; import * as ZSpec from '../../../zspec'; import * as Zdo from '../../../zspec/zdo'; +import {StartResult} from '../../tstype'; import * as ZnpConstants from '../constants'; import {DevStates, NvItemsIds, ZnpCommandStatus} from '../constants/common'; import * as ZStackModels from '../models'; @@ -22,11 +23,6 @@ import ZStackAdapter from './zStackAdapter'; const NS = 'zh:adapter:zstack:manager'; -/** - * Startup strategy is internally used to determine required startup method. - */ -type StartupStrategy = 'startup' | 'restoreBackup' | 'startCommissioning'; - /** * ZNP Adapter Manager is responsible for handling adapter startup, network commissioning, * configuration backup and restore. @@ -38,8 +34,7 @@ export class ZnpAdapterManager { private znp: Znp; private adapter: ZStackAdapter; private options: ZStackModels.StartupOptions; - // @ts-expect-error initialized in `start()` - private nwkOptions: Models.NetworkOptions; + private nwkOptions!: Models.NetworkOptions; public constructor(adapter: ZStackAdapter, znp: Znp, options: ZStackModels.StartupOptions) { this.znp = znp; @@ -49,228 +44,77 @@ export class ZnpAdapterManager { this.backup = new AdapterBackup(this.znp, this.nv, this.options.backupPath); } - /** - * Performs ZNP adapter startup. After this method returns the adapter is configured, endpoints are registered - * and network is ready to process frames. - */ - public async start(): Promise { + public async init(): Promise { logger.debug(`beginning znp startup`, NS); this.nwkOptions = await this.parseConfigNetworkOptions(this.options.networkOptions); await this.nv.init(); + } - /* determine startup strategy */ - const strategy = await this.determineStrategy(); - logger.debug(`determined startup strategy: ${strategy}`, NS); - - /* perform coordinator startup based on determined strategy */ - let result: TsType.StartResult; - switch (strategy) { - case 'startup': { - await this.beginStartup(); - result = 'resumed'; - break; - } - case 'restoreBackup': { - if (this.options.version === ZnpVersion.zStack12) { - logger.debug(`performing recommissioning instead of restore for z-stack 1.2`, NS); - await this.beginCommissioning(this.nwkOptions); - await this.beginStartup(); - } else { - await this.beginRestore(); - } - result = 'restored'; - break; - } - case 'startCommissioning': { - if (this.options.version === ZnpVersion.zStack12) { - const hasConfigured = await this.nv.readItem(NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK1, 0, Structs.hasConfigured); - await this.beginCommissioning(this.nwkOptions); - await this.beginStartup(); - result = hasConfigured && hasConfigured.isConfigured() ? 'reset' : 'restored'; - } else { - await this.beginCommissioning(this.nwkOptions); - result = 'reset'; - } - break; + public async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { + /* acquire data from adapter */ + const hasConfiguredNvId = + this.options.version === ZnpVersion.zStack12 ? NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK1 : NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK3; + const hasConfigured = await this.nv.readItem(hasConfiguredNvId, 0, Structs.hasConfigured); + + if (hasConfigured && hasConfigured.isConfigured()) { + const nib = await this.nv.readItem(NvItemsIds.NIB, 0, Structs.nib); + + if (nib) { + return [true, nib.nwkPanId, nib.extendedPANID]; } } - /* register endpoints */ - await this.registerEndpoints(); + return [false, undefined, undefined]; + } - /* add green power group */ - await this.addToGroup(242, this.options.greenPowerGroup); + public async resume(): Promise { + await this.beginStartup(); + } - return result; + public async restore(backup: Models.Backup): Promise { + if (this.options.version === ZnpVersion.zStack12) { + logger.debug(`performing recommissioning instead of restore for z-stack 1.2`, NS); + await this.leaveNetwork(); + await this.beginCommissioning(this.nwkOptions); + await this.beginStartup(); + } else { + await this.beginRestore(backup); + } + + return 'restored'; } - /** - * Internal function to determine startup strategy. The strategy determination flow is described in - * [this GitHub issue comment](https://github.com/Koenkk/zigbee-herdsman/issues/286#issuecomment-761029689). - */ - private async determineStrategy(): Promise { - logger.debug('determining znp startup strategy', NS); + public async reset(): Promise { + if (this.options.version === ZnpVersion.zStack12) { + const hasConfigured = await this.nv.readItem(NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK1, 0, Structs.hasConfigured); + await this.leaveNetwork(); + await this.beginCommissioning(this.nwkOptions); + await this.beginStartup(); - /* acquire data from adapter */ - const hasConfiguredNvId = - this.options.version === ZnpVersion.zStack12 ? NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK1 : NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK3; - const hasConfigured = await this.nv.readItem(hasConfiguredNvId, 0, Structs.hasConfigured); - const nib = await this.nv.readItem(NvItemsIds.NIB, 0, Structs.nib); + // XXX: forces to return `StartResult | void` from `adapter.formNetwork` (used for zstack only) + return hasConfigured && hasConfigured.isConfigured() ? 'reset' : 'restored'; + } else { + await this.beginCommissioning(this.nwkOptions); + + return 'reset'; + } + } + + public async getNetworkKey(): Promise { const preconfiguredKey = this.options.version === ZnpVersion.zStack12 ? Structs.nwkKey( (await this.znp.requestWithReply(Subsystem.SAPI, 'readConfiguration', {configid: NvItemsIds.PRECFGKEY})).payload.value, ) : await this.nv.readItem(NvItemsIds.PRECFGKEY, 0, Structs.nwkKey); - let activeKeyInfo = await this.nv.readItem(NvItemsIds.NWK_ACTIVE_KEY_INFO, 0, Structs.nwkKeyDescriptor); - let alternateKeyInfo = await this.nv.readItem(NvItemsIds.NWK_ALTERN_KEY_INFO, 0, Structs.nwkKeyDescriptor); - - /* Z-Stack 1.2 does not provide key info entries */ - if (this.options.version === ZnpVersion.zStack12) { - activeKeyInfo = Structs.nwkKeyDescriptor(); - activeKeyInfo.key = Buffer.from(preconfiguredKey.key); - alternateKeyInfo = Structs.nwkKeyDescriptor(); - alternateKeyInfo.key = Buffer.from(preconfiguredKey.key); - } - - /* get backup if available and supported by target */ - const backup = await this.backup.getStoredBackup(); - - /* special treatment for incorrectly reversed Extended PAN IDs from previous releases */ - const isExtendedPanIdReversed = nib && this.nwkOptions.extendedPanId.equals(Buffer.from(nib.extendedPANID).reverse()); - - /* istanbul ignore next */ - const configMatchesAdapter = - nib && - // Don't check for channel anymore because channel change is supported. - // Utils.compareChannelLists(this.nwkOptions.channelList, nib.channelList) && - this.nwkOptions.panId === nib.nwkPanId && - (this.nwkOptions.extendedPanId.equals(nib.extendedPANID) || - /* exception for migration from previous code-base */ - isExtendedPanIdReversed || - /* exception for some adapters which may actually use 0xdddddddddddddddd as EPID (backward compatibility) */ - this.nwkOptions.hasDefaultExtendedPanId) && - this.nwkOptions.networkKey.equals(preconfiguredKey.key) && - this.nwkOptions.networkKey.equals(activeKeyInfo.key) && - this.nwkOptions.networkKey.equals(alternateKeyInfo.key); - - const backupMatchesAdapter = - backup && - nib && - backup.networkOptions.panId === nib.nwkPanId && - backup.networkOptions.extendedPanId.equals(nib.extendedPANID) && - Utils.compareChannelLists(backup.networkOptions.channelList, nib.channelList) && - backup.networkOptions.networkKey.equals(activeKeyInfo.key); - - const configMatchesBackup = backup && Utils.compareNetworkOptions(this.nwkOptions, backup.networkOptions, true); - - const checkRestoreVersionCompatibility = (): void => { - if ( - this.options.version === ZnpVersion.zStack12 && - backup && - backup.znp?.version !== undefined && - backup.znp.version !== ZnpVersion.zStack12 - ) { - throw new Error( - `your backup is from newer platform version (Z-Stack 3.0.x+) and cannot be restored onto Z-Stack 1.2 adapter - please remove backup before proceeding`, - ); - } - }; - /* Determine startup strategy */ - if (!hasConfigured || !hasConfigured.isConfigured() || !nib) { - /* Adapter is not configured or not commissioned */ - logger.debug('(stage-1) adapter is not configured / not commissioned', NS); - if (configMatchesBackup) { - /* Adapter backup is available and matches configuration */ - logger.debug('(stage-2) configuration matches backup', NS); - checkRestoreVersionCompatibility(); - return 'restoreBackup'; - } else { - /* Adapter backup is either not available or does not match configuration */ - if (!backup) { - logger.debug('(stage-2) adapter backup does not exist', NS); - } else { - logger.debug('(stage-2) configuration does not match backup', NS); - } - return 'startCommissioning'; - } - } else { - /* Adapter is configured and commissioned */ - logger.debug('(stage-1) adapter is configured', NS); - - if (configMatchesAdapter) { - /* Warn if EPID is reversed (backward-compat) */ - if (isExtendedPanIdReversed) { - logger.debug('(stage-2) extended pan id is reversed', NS); - logger.warning( - `Extended PAN ID is reversed (expected=${this.nwkOptions.extendedPanId.toString('hex')}, actual=${nib.extendedPANID.toString('hex')})`, - NS, - ); - } + return Buffer.from(preconfiguredKey.key); + } - /* Configuration matches adapter state - regular startup */ - logger.debug('(stage-2) adapter state matches configuration', NS); - return 'startup'; - } else { - /* Configuration does not match adapter state */ - logger.debug('(stage-2) adapter state does not match configuration', NS); - if (backup) { - /* Backup is present */ - logger.debug('(stage-3) got adapter backup', NS); - if (backupMatchesAdapter) { - /* Backup matches adapter state */ - logger.debug('(stage-4) adapter state matches backup', NS); - logger.error(`Configuration is not consistent with adapter state/backup!`, NS); - logger.error(`- PAN ID: configured=${this.nwkOptions.panId}, adapter=${nib.nwkPanId}`, NS); - logger.error( - `- Extended PAN ID: configured=${this.nwkOptions.extendedPanId.toString('hex')}, adapter=${nib.extendedPANID.toString('hex')}`, - NS, - ); - logger.error( - `- Network Key: configured=${this.nwkOptions.networkKey.toString('hex')}, adapter=${activeKeyInfo.key.toString('hex')}`, - NS, - ); - logger.error( - `- Channel List: configured=${this.nwkOptions.channelList.toString()}, adapter=${Utils.unpackChannelList(nib.channelList).toString()}`, - NS, - ); - logger.error(`Please update configuration to prevent further issues.`, NS); - logger.error( - `If you wish to re-commission your network, please remove coordinator backup at ${this.options.backupPath}.`, - NS, - ); - logger.error(`Re-commissioning your network will require re-pairing of all devices!`, NS); - if (this.options.adapterOptions.forceStartWithInconsistentAdapterConfiguration) { - logger.error( - `Running despite adapter configuration mismatch as configured. Please update the adapter to compatible firmware and recreate your network as soon as possible.`, - NS, - ); - return 'startup'; - } else { - throw new Error('startup failed - configuration-adapter mismatch - see logs above for more information'); - } - } else { - /* Backup does not match adapter state */ - logger.debug('(stage-4) adapter state does not match backup', NS); - if (configMatchesBackup) { - /* Adapter backup matches configuration */ - logger.debug('(stage-5) adapter backup matches configuration', NS); - checkRestoreVersionCompatibility(); - return 'restoreBackup'; - } else { - /* Adapter backup does not match configuration */ - logger.debug('(stage-5) adapter backup does not match configuration', NS); - return 'startCommissioning'; - } - } - } else { - /* Configuration mismatches adapter and no backup is available */ - logger.debug('(stage-3) configuration-adapter mismatch (no backup)', NS); - return 'startCommissioning'; - } - } - } + public async leaveNetwork(): Promise { + /* clear and reset the adapter */ + await this.nv.deleteItem(NvItemsIds.NIB); + await this.clearAdapter(); } /** @@ -295,13 +139,7 @@ export class ZnpAdapterManager { /** * Internal method to perform adapter restore. */ - private async beginRestore(): Promise { - const backup = await this.backup.getStoredBackup(); - /* istanbul ignore next */ - if (!backup) { - throw Error('Cannot restore backup - none is available'); - } - + private async beginRestore(backup: Models.Backup): Promise { /* generate random provisioning network parameters */ const provisioningNwkOptions: Models.NetworkOptions = { panId: 1 + Math.round(Math.random() * 65532), @@ -311,6 +149,8 @@ export class ZnpAdapterManager { networkKeyDistribute: false, }; + await this.leaveNetwork(); + /* commission provisioning network */ logger.debug('commissioning random provisioning network:', NS); logger.debug(` - panId: ${provisioningNwkOptions.panId}`, NS); @@ -351,10 +191,6 @@ export class ZnpAdapterManager { throw new Error(`network commissioning failed - cannot use pan id 65535`); } - /* clear and reset the adapter */ - await this.nv.deleteItem(NvItemsIds.NIB); - await this.clearAdapter(); - /* commission the network as per parameters */ await this.updateCommissioningNvItems(nwkOptions); logger.debug('beginning network commissioning', NS); @@ -453,7 +289,7 @@ export class ZnpAdapterManager { /** * Registers endpoints before beginning normal operation. */ - private async registerEndpoints(): Promise { + public async registerEndpoints(): Promise { const clusterId = Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST; const zdoPayload = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterId, ZSpec.COORDINATOR_ADDRESS); const response = await this.adapter.sendZdo(ZSpec.BLANK_EUI64, ZSpec.COORDINATOR_ADDRESS, clusterId, zdoPayload, false); @@ -480,11 +316,12 @@ export class ZnpAdapterManager { * @param endpoint Endpoint index to add. * @param group Target group index. */ - private async addToGroup(endpoint: number, group: number): Promise { + public async addToGroup(endpoint: number, group: number): Promise { const result = await this.znp.requestWithReply(5, 'extFindGroup', {endpoint, groupid: group}, undefined, undefined, [ ZnpCommandStatus.SUCCESS, ZnpCommandStatus.FAILURE, ]); + if (result.payload.status === ZnpCommandStatus.FAILURE) { await this.znp.request(5, 'extAddGroup', {endpoint, groupid: group, namelen: 0, groupname: []}); } diff --git a/src/adapter/z-stack/adapter/zStackAdapter.ts b/src/adapter/z-stack/adapter/zStackAdapter.ts index f174ce2882..575260f1d1 100644 --- a/src/adapter/z-stack/adapter/zStackAdapter.ts +++ b/src/adapter/z-stack/adapter/zStackAdapter.ts @@ -65,7 +65,7 @@ class ZStackAdapter extends Adapter { private transactionID: number; // @ts-expect-error initialized in `start` private version: { - product: number; + product: ZnpVersion; transportrev: number; majorrel: number; minorrel: number; @@ -138,12 +138,20 @@ class ZStackAdapter extends Adapter { this.adapterManager = new ZnpAdapterManager(this, this.znp, { backupPath: this.backupPath, version: this.version.product, - greenPowerGroup: ZSpec.GP_GROUP_ID, networkOptions: this.networkOptions, adapterOptions: this.adapterOptions, }); - const startResult = this.adapterManager.start(); + const startResult = await this.initNetwork(); + + if (startResult === 'resumed') { + await this.adapterManager.resume(); + } + + await this.adapterManager.registerEndpoints(); + + /* add green power group */ + await this.adapterManager.addToGroup(ZSpec.GP_ENDPOINT, ZSpec.GP_GROUP_ID); if (this.adapterOptions.disableLED) { // Wait a bit for adapter to startup, otherwise led doesn't disable (tested with CC2531) @@ -155,7 +163,7 @@ class ZStackAdapter extends Adapter { await this.znp.request(Subsystem.SYS, 'stackTune', {operation: 0, value: this.adapterOptions.transmitPower}); } - return await startResult; + return startResult; } public async stop(): Promise { @@ -163,6 +171,33 @@ class ZStackAdapter extends Adapter { await this.znp.close(); } + /** + * Check the network status on the adapter (execute the necessary pre-steps to be able to get it). + * WARNING: This is a one-off. Should not be called outside of `initNetwork`. + */ + protected async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { + return await this.adapterManager.initHasNetwork(); + } + + public async leaveNetwork(): Promise { + return await this.adapterManager.leaveNetwork(); + } + + /** + * If backup is defined, form network from backup, otherwise from config. + */ + public async formNetwork(backup?: Models.Backup): Promise { + if (backup) { + return await this.adapterManager.restore(backup); + } else { + return await this.adapterManager.reset(); + } + } + + public async getNetworkKey(): Promise { + return await this.adapterManager.getNetworkKey(); + } + public async getCoordinatorIEEE(): Promise { return await this.queue.execute(async () => { this.checkInterpanLock(); @@ -898,8 +933,25 @@ class ZStackAdapter extends Adapter { return true; } + public checkBackup(backup: Models.Backup): void { + if (!backup.znp?.trustCenterLinkKeySeed) { + throw new Error(`Current backup file is not for zStack.`); + } + + if ( + this.version.product === ZnpVersion.zStack12 && + backup && + backup.znp?.version !== undefined && + backup.znp.version !== ZnpVersion.zStack12 + ) { + throw new Error( + `your backup is from newer platform version (Z-Stack 3.0.x+) and cannot be restored onto Z-Stack 1.2 adapter - please remove backup before proceeding`, + ); + } + } + public async backup(ieeeAddressesInDatabase: string[]): Promise { - return await this.adapterManager.backup.createBackup(ieeeAddressesInDatabase); + return await this.adapterManager.backup.createBackup(ieeeAddressesInDatabase, this.getStoredBackup.bind(this)); } public async setChannelInterPAN(channel: number): Promise { diff --git a/src/adapter/z-stack/models/startup-options.ts b/src/adapter/z-stack/models/startup-options.ts index 0f0c945732..23cf043bab 100644 --- a/src/adapter/z-stack/models/startup-options.ts +++ b/src/adapter/z-stack/models/startup-options.ts @@ -7,7 +7,6 @@ import {ZnpVersion} from '../adapter/tstype'; export interface StartupOptions { version: ZnpVersion; networkOptions: TsType.NetworkOptions; - greenPowerGroup: number; backupPath: string; adapterOptions: TsType.AdapterOptions; } From 641d170816ba956f2f4a34750235dfab70bb42b1 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sat, 14 Dec 2024 20:56:01 +0100 Subject: [PATCH 10/16] cleanup --- src/adapter/z-stack/adapter/manager.ts | 8 ++------ src/adapter/z-stack/adapter/zStackAdapter.ts | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/adapter/z-stack/adapter/manager.ts b/src/adapter/z-stack/adapter/manager.ts index aa8d679986..aabdbfa135 100644 --- a/src/adapter/z-stack/adapter/manager.ts +++ b/src/adapter/z-stack/adapter/manager.ts @@ -71,7 +71,7 @@ export class ZnpAdapterManager { await this.beginStartup(); } - public async restore(backup: Models.Backup): Promise { + public async restore(backup: Models.Backup): Promise { if (this.options.version === ZnpVersion.zStack12) { logger.debug(`performing recommissioning instead of restore for z-stack 1.2`, NS); await this.leaveNetwork(); @@ -80,11 +80,9 @@ export class ZnpAdapterManager { } else { await this.beginRestore(backup); } - - return 'restored'; } - public async reset(): Promise { + public async reset(): Promise { if (this.options.version === ZnpVersion.zStack12) { const hasConfigured = await this.nv.readItem(NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK1, 0, Structs.hasConfigured); await this.leaveNetwork(); @@ -95,8 +93,6 @@ export class ZnpAdapterManager { return hasConfigured && hasConfigured.isConfigured() ? 'reset' : 'restored'; } else { await this.beginCommissioning(this.nwkOptions); - - return 'reset'; } } diff --git a/src/adapter/z-stack/adapter/zStackAdapter.ts b/src/adapter/z-stack/adapter/zStackAdapter.ts index 575260f1d1..4472dcaf57 100644 --- a/src/adapter/z-stack/adapter/zStackAdapter.ts +++ b/src/adapter/z-stack/adapter/zStackAdapter.ts @@ -186,9 +186,9 @@ class ZStackAdapter extends Adapter { /** * If backup is defined, form network from backup, otherwise from config. */ - public async formNetwork(backup?: Models.Backup): Promise { + public async formNetwork(backup?: Models.Backup): Promise { if (backup) { - return await this.adapterManager.restore(backup); + await this.adapterManager.restore(backup); } else { return await this.adapterManager.reset(); } From 1fea6d1b5b1e9a5214699563dc1f5fc4c16629a6 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sat, 14 Dec 2024 21:05:46 +0100 Subject: [PATCH 11/16] fix --- src/adapter/z-stack/adapter/manager.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/adapter/z-stack/adapter/manager.ts b/src/adapter/z-stack/adapter/manager.ts index aabdbfa135..c348de3840 100644 --- a/src/adapter/z-stack/adapter/manager.ts +++ b/src/adapter/z-stack/adapter/manager.ts @@ -44,13 +44,11 @@ export class ZnpAdapterManager { this.backup = new AdapterBackup(this.znp, this.nv, this.options.backupPath); } - public async init(): Promise { + public async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { logger.debug(`beginning znp startup`, NS); this.nwkOptions = await this.parseConfigNetworkOptions(this.options.networkOptions); await this.nv.init(); - } - public async initHasNetwork(): Promise<[true, panID: number, extendedPanID: Buffer] | [false, panID: undefined, extendedPanID: undefined]> { /* acquire data from adapter */ const hasConfiguredNvId = this.options.version === ZnpVersion.zStack12 ? NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK1 : NvItemsIds.ZNP_HAS_CONFIGURED_ZSTACK3; From 727bdb623a6a3c3d35d117e64abdceaa83aef3a3 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:27:34 +0100 Subject: [PATCH 12/16] fix deconz form channel --- src/adapter/deconz/adapter/deconzAdapter.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/adapter/deconz/adapter/deconzAdapter.ts b/src/adapter/deconz/adapter/deconzAdapter.ts index bea48cc2da..e21d906f99 100644 --- a/src/adapter/deconz/adapter/deconzAdapter.ts +++ b/src/adapter/deconz/adapter/deconzAdapter.ts @@ -129,6 +129,11 @@ class DeconzAdapter extends Adapter { // this path should never be reached throw new Error('This adapter does not support backup'); } else { + await this.driver.writeParameterRequest( + PARAM.PARAM.Network.CHANNEL_MASK, + ZSpec.Utils.channelsToUInt32Mask(this.networkOptions.channelList), + ); + await Wait(500); await this.driver.writeParameterRequest(PARAM.PARAM.Network.PAN_ID, this.networkOptions.panID); await Wait(500); await this.driver.writeParameterRequest(PARAM.PARAM.Network.APS_EXT_PAN_ID, this.networkOptions.extendedPanID!); From 7cdafede3a4e63cc569f7c8c068f2d18569f5d05 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:37:01 +0100 Subject: [PATCH 13/16] fix merge --- src/adapter/deconz/adapter/deconzAdapter.ts | 1 + src/adapter/ember/adapter/emberAdapter.ts | 6 +++--- src/adapter/ezsp/driver/driver.ts | 2 -- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/adapter/deconz/adapter/deconzAdapter.ts b/src/adapter/deconz/adapter/deconzAdapter.ts index 33ce420a51..ab54512d6f 100644 --- a/src/adapter/deconz/adapter/deconzAdapter.ts +++ b/src/adapter/deconz/adapter/deconzAdapter.ts @@ -146,6 +146,7 @@ export class DeconzAdapter extends Adapter { await wait(2000); await this.driver.changeNetworkStateRequest(PARAM.PARAM.Network.NET_CONNECTED); await wait(2000); + } } public async getNetworkKey(): Promise { diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 6f8f389dd6..1ff1931e76 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -5,8 +5,8 @@ import path from 'path'; import equals from 'fast-deep-equal/es6'; import {Adapter, TsType} from '../..'; -import {Backup, UnifiedBackupStorage} from '../../../models'; -import {BackupUtils, Queue, wait} from '../../../utils'; +import {Backup} from '../../../models'; +import {Queue, wait} from '../../../utils'; import {logger} from '../../../utils/logger'; import * as ZSpec from '../../../zspec'; import {EUI64, ExtendedPanId, NodeId, PanId} from '../../../zspec/tstypes'; @@ -1375,7 +1375,7 @@ export class EmberAdapter extends Adapter { 'Leave network', ); - await Wait(200); // settle down + await wait(200); // settle down } public async formNetwork(backup?: Backup): Promise { diff --git a/src/adapter/ezsp/driver/driver.ts b/src/adapter/ezsp/driver/driver.ts index 22f29a9345..49c31f65fd 100644 --- a/src/adapter/ezsp/driver/driver.ts +++ b/src/adapter/ezsp/driver/driver.ts @@ -2,8 +2,6 @@ import {EventEmitter} from 'events'; -import equals from 'fast-deep-equal/es6'; - import {wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; import * as ZSpec from '../../../zspec'; From 823881758cc8b3ea75624076f6679fd218cafcaf Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:40:06 +0100 Subject: [PATCH 14/16] fix --- src/adapter/adapter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index d29c3f3145..2839e4dc35 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -21,10 +21,10 @@ interface AdapterEventMap { } type AdapterConstructor = new ( - networkOptions: TsType.NetworkOptions, - serialPortOptions: TsType.SerialPortOptions, + networkOptions: NetworkOptions, + serialPortOptions: SerialPortOptions, backupPath: string, - adapterOptions: TsType.AdapterOptions, + adapterOptions: AdapterOptions, ) => Adapter; export abstract class Adapter extends events.EventEmitter { From 5baf28eb4882a0a34908d2ec905791d575e6b4dd Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:54:47 +0100 Subject: [PATCH 15/16] fix --- test/adapter/ember/emberAdapter.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/adapter/ember/emberAdapter.test.ts b/test/adapter/ember/emberAdapter.test.ts index f3985dc939..ee656c3479 100644 --- a/test/adapter/ember/emberAdapter.test.ts +++ b/test/adapter/ember/emberAdapter.test.ts @@ -241,7 +241,7 @@ const mockEzspGetVersionStruct = vi.fn(() => type: EmberVersionType.GA, } as EmberVersion, ]), -)); +); const mockEzspSetConfigurationValue = vi.fn(() => Promise.resolve(SLStatus.OK)); const mockEzspSetValue = vi.fn(() => Promise.resolve(SLStatus.OK)); const mockEzspSetPolicy = vi.fn(() => Promise.resolve(SLStatus.OK)); @@ -266,7 +266,7 @@ const mockEzspGetApsKeyInfo = vi.fn(() => ttlInSeconds: 0, } as SecManAPSKeyMetadata, ]), -)); +); const mockEzspSetRadioPower = vi.fn(() => Promise.resolve(SLStatus.OK)); const mockEzspImportTransientKey = vi.fn(() => Promise.resolve(SLStatus.OK)); const mockEzspClearTransientLinkKeys = vi.fn(() => Promise.resolve(SLStatus.OK)); From d30a93b4ea8d0996d8f521acadfcbd852221e370 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:58:19 +0100 Subject: [PATCH 16/16] fix coverage ignore --- src/adapter/ember/adapter/emberAdapter.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 75df06e7d0..76e583b840 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -818,7 +818,7 @@ export class EmberAdapter extends Adapter { ); } - /* istanbul ignore next */ + /* v8 ignore next */ const appKeyRequestsPolicy = ALLOW_APP_KEY_REQUESTS ? EzspDecisionId.ALLOW_APP_KEY_REQUESTS : EzspDecisionId.DENY_APP_KEY_REQUESTS; status = await this.emberSetEzspPolicy(EzspPolicyId.APP_KEY_REQUEST_POLICY, appKeyRequestsPolicy); @@ -1374,13 +1374,14 @@ export class EmberAdapter extends Adapter { if (backup) { logger.info(`[INIT FORM] Forming from backup.`, NS); // `backup` valid in this `action` path (not detected by TS) - /* istanbul ignore next */ + /* v8 ignore start */ const keyList: LinkKeyBackupData[] = backup!.devices.map((device) => ({ deviceEui64: ZSpec.Utils.eui64BEBufferToHex(device.ieeeAddress), key: {contents: device.linkKey!.key}, outgoingFrameCounter: device.linkKey!.txCounter, incomingFrameCounter: device.linkKey!.rxCounter, })); + /* v8 ignore stop */ // before forming await this.importLinkKeys(keyList);