diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 13f08cb47f..1928c49ce3 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -1678,6 +1678,7 @@ export class EmberAdapter extends Adapter { panID, extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(extendedPanID)), channel, + nwkUpdateID: this.networkCache.parameters.nwkUpdateId, }; }); } diff --git a/src/adapter/ezsp/adapter/ezspAdapter.ts b/src/adapter/ezsp/adapter/ezspAdapter.ts index f019a39937..d5c0f4d311 100644 --- a/src/adapter/ezsp/adapter/ezspAdapter.ts +++ b/src/adapter/ezsp/adapter/ezspAdapter.ts @@ -462,6 +462,7 @@ export class EZSPAdapter extends Adapter { panID: this.driver.networkParams.panId, extendedPanID: ZSpec.Utils.eui64LEBufferToHex(this.driver.networkParams.extendedPanId), channel: this.driver.networkParams.radioChannel, + nwkUpdateID: this.driver.networkParams.nwkUpdateId, }; } diff --git a/src/adapter/tstype.ts b/src/adapter/tstype.ts index 4b7e75801d..e9ccd704a7 100644 --- a/src/adapter/tstype.ts +++ b/src/adapter/tstype.ts @@ -74,4 +74,5 @@ export interface NetworkParameters { panID: number; extendedPanID: string; // `0x${string}` same as IEEE address channel: number; + nwkUpdateID?: number; } diff --git a/src/controller/controller.ts b/src/controller/controller.ts index d7e27817f9..78ba098129 100644 --- a/src/controller/controller.ts +++ b/src/controller/controller.ts @@ -144,10 +144,17 @@ export class Controller extends events.EventEmitter { const netParams = await this.getNetworkParameters(); const configuredChannel = this.options.network.channelList[0]; const adapterChannel = netParams.channel; + // According to the Zigbee specification: + // When broadcasting a Mgmt_NWK_Update_req to notify devices of a new channel, the nwkUpdateId parameter should be incremented in the NIB and included in the Mgmt_NWK_Update_req. + // The valid range of nwkUpdateId is 0x00 to 0xFF, and it should wrap back to 0 if necessary. + let nwkUpdateID = netParams.nwkUpdateID ?? 0; + if (++nwkUpdateID > 0xff) { + nwkUpdateID = 0x00; + } if (configuredChannel != adapterChannel) { logger.info(`Configured channel '${configuredChannel}' does not match adapter channel '${adapterChannel}', changing channel`, NS); - await this.changeChannel(adapterChannel, configuredChannel); + await this.changeChannel(adapterChannel, configuredChannel, nwkUpdateID); } } @@ -496,11 +503,19 @@ export class Controller extends events.EventEmitter { /** * Broadcast a network-wide channel change. */ - private async changeChannel(oldChannel: number, newChannel: number): Promise { + private async changeChannel(oldChannel: number, newChannel: number, nwkUpdateID: number): Promise { logger.warning(`Changing channel from '${oldChannel}' to '${newChannel}'`, NS); const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST; - const zdoPayload = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterId, [newChannel], 0xfe, undefined, 0, undefined); + const zdoPayload = Zdo.Buffalo.buildRequest( + this.adapter.hasZdoMessageOverhead, + clusterId, + [newChannel], + 0xfe, + undefined, + nwkUpdateID, + undefined, + ); await this.adapter.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true); logger.info(`Channel changed to '${newChannel}'`, NS); diff --git a/test/adapter/ember/emberAdapter.test.ts b/test/adapter/ember/emberAdapter.test.ts index 4284e922d0..759e2a1b03 100644 --- a/test/adapter/ember/emberAdapter.test.ts +++ b/test/adapter/ember/emberAdapter.test.ts @@ -2249,6 +2249,7 @@ describe('Ember Adapter Layer', () => { panID: DEFAULT_NETWORK_OPTIONS.panID, extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(DEFAULT_NETWORK_OPTIONS.extendedPanID!)), channel: DEFAULT_NETWORK_OPTIONS.channelList[0], + nwkUpdateID: 0, } as TsType.NetworkParameters); expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(0); }); @@ -2260,6 +2261,7 @@ describe('Ember Adapter Layer', () => { panID: DEFAULT_NETWORK_OPTIONS.panID, extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(DEFAULT_NETWORK_OPTIONS.extendedPanID!)), channel: DEFAULT_NETWORK_OPTIONS.channelList[0], + nwkUpdateID: 0, } as TsType.NetworkParameters); expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(1); }); diff --git a/test/controller.test.ts b/test/controller.test.ts index a120bbd6d4..fbc8f94c81 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -37,6 +37,44 @@ const mockLogger = { error: vi.fn(), }; +const mockDummyBackup: Models.Backup = { + networkOptions: { + panId: 6755, + extendedPanId: Buffer.from('deadbeef01020304', 'hex'), + channelList: [11], + networkKey: Buffer.from('a1a2a3a4a5a6a7a8b1b2b3b4b5b6b7b8', 'hex'), + networkKeyDistribute: false, + }, + coordinatorIeeeAddress: Buffer.from('0102030405060708', 'hex'), + logicalChannel: 11, + networkUpdateId: 0, + securityLevel: 5, + znp: { + version: 1, + }, + networkKeyInfo: { + sequenceNumber: 0, + frameCounter: 10000, + }, + devices: [ + { + networkAddress: 1001, + ieeeAddress: Buffer.from('c1c2c3c4c5c6c7c8', 'hex'), + isDirectChild: false, + }, + { + networkAddress: 1002, + ieeeAddress: Buffer.from('d1d2d3d4d5d6d7d8', 'hex'), + isDirectChild: false, + linkKey: { + key: Buffer.from('f8f7f6f5f4f3f2f1e1e2e3e4e5e6e7e8', 'hex'), + rxCounter: 10000, + txCounter: 5000, + }, + }, + ], +}; + const mockAdapterEvents = {}; const mockAdapterWaitFor = vi.fn(); const mockAdapterSupportsDiscoverRoute = vi.fn(); @@ -56,6 +94,7 @@ const mocksendZclFrameToGroup = vi.fn(); const mocksendZclFrameToAll = vi.fn(); const mockAddInstallCode = vi.fn(); const mocksendZclFrameToEndpoint = vi.fn(); +const mockApaterBackup = vi.fn(() => Promise.resolve(mockDummyBackup)); let sendZdoResponseStatus = Zdo.Status.SUCCESS; const mockAdapterSendZdo = vi .fn() @@ -318,44 +357,6 @@ const getCluster = (key) => { return cluster; }; -const mockDummyBackup: Models.Backup = { - networkOptions: { - panId: 6755, - extendedPanId: Buffer.from('deadbeef01020304', 'hex'), - channelList: [11], - networkKey: Buffer.from('a1a2a3a4a5a6a7a8b1b2b3b4b5b6b7b8', 'hex'), - networkKeyDistribute: false, - }, - coordinatorIeeeAddress: Buffer.from('0102030405060708', 'hex'), - logicalChannel: 11, - networkUpdateId: 0, - securityLevel: 5, - znp: { - version: 1, - }, - networkKeyInfo: { - sequenceNumber: 0, - frameCounter: 10000, - }, - devices: [ - { - networkAddress: 1001, - ieeeAddress: Buffer.from('c1c2c3c4c5c6c7c8', 'hex'), - isDirectChild: false, - }, - { - networkAddress: 1002, - ieeeAddress: Buffer.from('d1d2d3d4d5d6d7d8', 'hex'), - isDirectChild: false, - linkKey: { - key: Buffer.from('f8f7f6f5f4f3f2f1e1e2e3e4e5e6e7e8', 'hex'), - rxCounter: 10000, - txCounter: 5000, - }, - }, - ], -}; - let dummyBackup; vi.mock('../src/adapter/z-stack/adapter/zStackAdapter', () => ({ @@ -368,9 +369,7 @@ vi.mock('../src/adapter/z-stack/adapter/zStackAdapter', () => ({ getCoordinatorIEEE: mockAdapterGetCoordinatorIEEE, reset: mockAdapterReset, supportsBackup: mockAdapterSupportsBackup, - backup: () => { - return mockDummyBackup; - }, + backup: mockApaterBackup, getCoordinatorVersion: () => { return {type: 'zStack', meta: {version: 1}}; }, @@ -1562,6 +1561,25 @@ describe('Controller', () => { const changeChannelSpy = vi.spyOn(controller, 'changeChannel'); await controller.start(); expect(mockAdapterGetNetworkParameters).toHaveBeenCalledTimes(1); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NWK_UPDATE_REQUEST, [15], 0xfe, undefined, 1, undefined); + expect(mockAdapterSendZdo).toHaveBeenCalledWith( + ZSpec.BLANK_EUI64, + ZSpec.BroadcastAddress.SLEEPY, + Zdo.ClusterId.NWK_UPDATE_REQUEST, + zdoPayload, + true, + ); + expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00'}); + expect(changeChannelSpy).toHaveBeenCalledTimes(1); + }); + + it('Change channel on start when nwkUpdateID is 0xff', async () => { + mockAdapterStart.mockReturnValueOnce('resumed'); + mockAdapterGetNetworkParameters.mockReturnValueOnce({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 25, nwkUpdateID: 0xff}); + // @ts-expect-error private + const changeChannelSpy = vi.spyOn(controller, 'changeChannel'); + await controller.start(); + expect(mockAdapterGetNetworkParameters).toHaveBeenCalledTimes(1); const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NWK_UPDATE_REQUEST, [15], 0xfe, undefined, 0, undefined); expect(mockAdapterSendZdo).toHaveBeenCalledWith( ZSpec.BLANK_EUI64,