Skip to content

Commit fc7a782

Browse files
CubeZ2mDeveloperdanyinhaoKoenkk
authored
fix: Fix some devices not moving to new channel after channel change (#1280)
Co-authored-by: 郑泽涛 <[email protected]> Co-authored-by: Koen Kanters <[email protected]>
1 parent 76d3de1 commit fc7a782

File tree

11 files changed

+107
-51
lines changed

11 files changed

+107
-51
lines changed

src/adapter/deconz/adapter/deconzAdapter.ts

+4
Original file line numberDiff line numberDiff line change
@@ -536,11 +536,15 @@ export class DeconzAdapter extends Adapter {
536536
const panid = await this.driver.readParameterRequest(PARAM.PARAM.Network.PAN_ID);
537537
const expanid = await this.driver.readParameterRequest(PARAM.PARAM.Network.APS_EXT_PAN_ID);
538538
const channel = await this.driver.readParameterRequest(PARAM.PARAM.Network.CHANNEL);
539+
// For some reason, reading NWK_UPDATE_ID always returns `null` (tested with `0x26780700` on Conbee II)
540+
// 0x24 was taken from https://github.com/zigpy/zigpy-deconz/blob/70910bc6a63e607332b4f12754ba470651eb878c/zigpy_deconz/api.py#L152
541+
// const nwkUpdateId = await this.driver.readParameterRequest(0x24 /*PARAM.PARAM.Network.NWK_UPDATE_ID*/);
539542

540543
return {
541544
panID: panid as number,
542545
extendedPanID: expanid as string, // read as `0x...`
543546
channel: channel as number,
547+
nwkUpdateID: 0 as number,
544548
};
545549
} catch (error) {
546550
const msg = 'get network parameters Error:' + error;

src/adapter/ember/adapter/emberAdapter.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,7 @@ export class EmberAdapter extends Adapter {
979979
Array.from(backup!.networkOptions.extendedPanId),
980980
backup!.logicalChannel,
981981
backup!.ezsp!.hashed_tclk!, // valid from getStoredBackup
982+
backup!.networkUpdateId,
982983
);
983984

984985
result = 'restored';
@@ -995,6 +996,7 @@ export class EmberAdapter extends Adapter {
995996
this.networkOptions.extendedPanID!,
996997
this.networkOptions.channelList[0],
997998
randomBytes(EMBER_ENCRYPTION_KEY_SIZE), // rnd TC link key
999+
0,
9981000
);
9991001

10001002
result = 'reset';
@@ -1041,6 +1043,7 @@ export class EmberAdapter extends Adapter {
10411043
extendedPanId: ExtendedPanId,
10421044
radioChannel: number,
10431045
tcLinkKey: Buffer,
1046+
nwkUpdateId: number,
10441047
): Promise<void> {
10451048
const state: EmberInitialSecurityState = {
10461049
bitmask:
@@ -1100,7 +1103,7 @@ export class EmberAdapter extends Adapter {
11001103
radioChannel,
11011104
joinMethod: EmberJoinMethod.MAC_ASSOCIATION,
11021105
nwkManagerId: ZSpec.COORDINATOR_ADDRESS,
1103-
nwkUpdateId: 0,
1106+
nwkUpdateId,
11041107
channels: ZSpec.ALL_802_15_4_CHANNELS_MASK,
11051108
};
11061109

@@ -1678,6 +1681,7 @@ export class EmberAdapter extends Adapter {
16781681
panID,
16791682
extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(extendedPanID)),
16801683
channel,
1684+
nwkUpdateID: this.networkCache.parameters.nwkUpdateId,
16811685
};
16821686
});
16831687
}

src/adapter/ezsp/adapter/ezspAdapter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ export class EZSPAdapter extends Adapter {
462462
panID: this.driver.networkParams.panId,
463463
extendedPanID: ZSpec.Utils.eui64LEBufferToHex(this.driver.networkParams.extendedPanId),
464464
channel: this.driver.networkParams.radioChannel,
465+
nwkUpdateID: this.driver.networkParams.nwkUpdateId,
465466
};
466467
}
467468

src/adapter/tstype.ts

+1
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,5 @@ export interface NetworkParameters {
7474
panID: number;
7575
extendedPanID: string; // `0x${string}` same as IEEE address
7676
channel: number;
77+
nwkUpdateID: number;
7778
}

src/adapter/z-stack/adapter/zStackAdapter.ts

+7
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,13 @@ export class ZStackAdapter extends Adapter {
890890
panID: result.payload.panid as number,
891891
extendedPanID: result.payload.extendedpanid as string, // read as IEEEADDR, so `0x${string}`
892892
channel: result.payload.channel as number,
893+
/**
894+
* Return a dummy nwkUpdateId of 0, the nwkUpdateId is used when changing channels however the
895+
* zstack API does not allow to set this value. Instead it automatically increments the nwkUpdateId
896+
* based on the value in the NIB.
897+
* https://github.com/Koenkk/zigbee-herdsman/pull/1280#discussion_r1947815987
898+
*/
899+
nwkUpdateID: 0,
893900
};
894901
}
895902

src/adapter/zboss/adapter/zbossAdapter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export class ZBOSSAdapter extends Adapter {
167167
panID,
168168
extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(extendedPanID)),
169169
channel,
170+
nwkUpdateID: 0,
170171
};
171172
});
172173
}

src/adapter/zigate/adapter/zigateAdapter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export class ZiGateAdapter extends Adapter {
175175
panID: result.payload.PANID as number,
176176
extendedPanID: result.payload.ExtPANID as string, // read as IEEEADDR, so `0x${string}`
177177
channel: result.payload.Channel as number,
178+
nwkUpdateID: 0 as number,
178179
};
179180
} catch (error) {
180181
throw new Error(`Get network parameters failed ${error}`);

src/controller/controller.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,11 @@ export class Controller extends events.EventEmitter<ControllerEventMap> {
144144
const netParams = await this.getNetworkParameters();
145145
const configuredChannel = this.options.network.channelList[0];
146146
const adapterChannel = netParams.channel;
147+
const nwkUpdateID = netParams.nwkUpdateID;
147148

148149
if (configuredChannel != adapterChannel) {
149150
logger.info(`Configured channel '${configuredChannel}' does not match adapter channel '${adapterChannel}', changing channel`, NS);
150-
await this.changeChannel(adapterChannel, configuredChannel);
151+
await this.changeChannel(adapterChannel, configuredChannel, nwkUpdateID);
151152
}
152153
}
153154

@@ -503,11 +504,26 @@ export class Controller extends events.EventEmitter<ControllerEventMap> {
503504
/**
504505
* Broadcast a network-wide channel change.
505506
*/
506-
private async changeChannel(oldChannel: number, newChannel: number): Promise<void> {
507+
private async changeChannel(oldChannel: number, newChannel: number, nwkUpdateID: number): Promise<void> {
507508
logger.warning(`Changing channel from '${oldChannel}' to '${newChannel}'`, NS);
508509

510+
// According to the Zigbee specification:
511+
// 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.
512+
// The valid range of nwkUpdateId is 0x00 to 0xFF, and it should wrap back to 0 if necessary.
513+
if (++nwkUpdateID > 0xff) {
514+
nwkUpdateID = 0x00;
515+
}
516+
509517
const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST;
510-
const zdoPayload = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterId, [newChannel], 0xfe, undefined, 0, undefined);
518+
const zdoPayload = Zdo.Buffalo.buildRequest(
519+
this.adapter.hasZdoMessageOverhead,
520+
clusterId,
521+
[newChannel],
522+
0xfe,
523+
undefined,
524+
nwkUpdateID,
525+
undefined,
526+
);
511527

512528
await this.adapter.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true);
513529
logger.info(`Channel changed to '${newChannel}'`, NS);

test/adapter/ember/emberAdapter.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2249,6 +2249,7 @@ describe('Ember Adapter Layer', () => {
22492249
panID: DEFAULT_NETWORK_OPTIONS.panID,
22502250
extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(DEFAULT_NETWORK_OPTIONS.extendedPanID!)),
22512251
channel: DEFAULT_NETWORK_OPTIONS.channelList[0],
2252+
nwkUpdateID: 0,
22522253
} as TsType.NetworkParameters);
22532254
expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(0);
22542255
});
@@ -2260,6 +2261,7 @@ describe('Ember Adapter Layer', () => {
22602261
panID: DEFAULT_NETWORK_OPTIONS.panID,
22612262
extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(DEFAULT_NETWORK_OPTIONS.extendedPanID!)),
22622263
channel: DEFAULT_NETWORK_OPTIONS.channelList[0],
2264+
nwkUpdateID: 0,
22632265
} as TsType.NetworkParameters);
22642266
expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(1);
22652267
});

test/adapter/z-stack/adapter.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3511,7 +3511,7 @@ describe('zstack-adapter', () => {
35113511
const result = await adapter.getNetworkParameters();
35123512
expect(mockZnpRequest).toHaveBeenCalledTimes(1);
35133513
expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'extNwkInfo', {});
3514-
expect(result).toStrictEqual({channel: 21, extendedPanID: '0x00124b0009d69f77', panID: 123});
3514+
expect(result).toStrictEqual({channel: 21, extendedPanID: '0x00124b0009d69f77', panID: 123, nwkUpdateID: 0});
35153515
});
35163516

35173517
it('Set interpan channel', async () => {

test/controller.test.ts

+65-46
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,44 @@ const mockLogger = {
3737
error: vi.fn(),
3838
};
3939

40+
const mockDummyBackup: Models.Backup = {
41+
networkOptions: {
42+
panId: 6755,
43+
extendedPanId: Buffer.from('deadbeef01020304', 'hex'),
44+
channelList: [11],
45+
networkKey: Buffer.from('a1a2a3a4a5a6a7a8b1b2b3b4b5b6b7b8', 'hex'),
46+
networkKeyDistribute: false,
47+
},
48+
coordinatorIeeeAddress: Buffer.from('0102030405060708', 'hex'),
49+
logicalChannel: 11,
50+
networkUpdateId: 0,
51+
securityLevel: 5,
52+
znp: {
53+
version: 1,
54+
},
55+
networkKeyInfo: {
56+
sequenceNumber: 0,
57+
frameCounter: 10000,
58+
},
59+
devices: [
60+
{
61+
networkAddress: 1001,
62+
ieeeAddress: Buffer.from('c1c2c3c4c5c6c7c8', 'hex'),
63+
isDirectChild: false,
64+
},
65+
{
66+
networkAddress: 1002,
67+
ieeeAddress: Buffer.from('d1d2d3d4d5d6d7d8', 'hex'),
68+
isDirectChild: false,
69+
linkKey: {
70+
key: Buffer.from('f8f7f6f5f4f3f2f1e1e2e3e4e5e6e7e8', 'hex'),
71+
rxCounter: 10000,
72+
txCounter: 5000,
73+
},
74+
},
75+
],
76+
};
77+
4078
const mockAdapterEvents = {};
4179
const mockAdapterWaitFor = vi.fn();
4280
const mockAdapterSupportsDiscoverRoute = vi.fn();
@@ -51,11 +89,12 @@ const mockAdapterReset = vi.fn();
5189
const mockAdapterStop = vi.fn();
5290
const mockAdapterStart = vi.fn().mockReturnValue('resumed');
5391
const mockAdapterGetCoordinatorIEEE = vi.fn().mockReturnValue('0x0000012300000000');
54-
const mockAdapterGetNetworkParameters = vi.fn().mockReturnValue({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 15});
92+
const mockAdapterGetNetworkParameters = vi.fn().mockReturnValue({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 15, nwkUpdateID: 0});
5593
const mocksendZclFrameToGroup = vi.fn();
5694
const mocksendZclFrameToAll = vi.fn();
5795
const mockAddInstallCode = vi.fn();
5896
const mocksendZclFrameToEndpoint = vi.fn();
97+
const mockApaterBackup = vi.fn(() => Promise.resolve(mockDummyBackup));
5998
let sendZdoResponseStatus = Zdo.Status.SUCCESS;
6099
const mockAdapterSendZdo = vi
61100
.fn()
@@ -318,44 +357,6 @@ const getCluster = (key) => {
318357
return cluster;
319358
};
320359

321-
const mockDummyBackup: Models.Backup = {
322-
networkOptions: {
323-
panId: 6755,
324-
extendedPanId: Buffer.from('deadbeef01020304', 'hex'),
325-
channelList: [11],
326-
networkKey: Buffer.from('a1a2a3a4a5a6a7a8b1b2b3b4b5b6b7b8', 'hex'),
327-
networkKeyDistribute: false,
328-
},
329-
coordinatorIeeeAddress: Buffer.from('0102030405060708', 'hex'),
330-
logicalChannel: 11,
331-
networkUpdateId: 0,
332-
securityLevel: 5,
333-
znp: {
334-
version: 1,
335-
},
336-
networkKeyInfo: {
337-
sequenceNumber: 0,
338-
frameCounter: 10000,
339-
},
340-
devices: [
341-
{
342-
networkAddress: 1001,
343-
ieeeAddress: Buffer.from('c1c2c3c4c5c6c7c8', 'hex'),
344-
isDirectChild: false,
345-
},
346-
{
347-
networkAddress: 1002,
348-
ieeeAddress: Buffer.from('d1d2d3d4d5d6d7d8', 'hex'),
349-
isDirectChild: false,
350-
linkKey: {
351-
key: Buffer.from('f8f7f6f5f4f3f2f1e1e2e3e4e5e6e7e8', 'hex'),
352-
rxCounter: 10000,
353-
txCounter: 5000,
354-
},
355-
},
356-
],
357-
};
358-
359360
let dummyBackup;
360361

361362
vi.mock('../src/adapter/z-stack/adapter/zStackAdapter', () => ({
@@ -368,9 +369,7 @@ vi.mock('../src/adapter/z-stack/adapter/zStackAdapter', () => ({
368369
getCoordinatorIEEE: mockAdapterGetCoordinatorIEEE,
369370
reset: mockAdapterReset,
370371
supportsBackup: mockAdapterSupportsBackup,
371-
backup: () => {
372-
return mockDummyBackup;
373-
},
372+
backup: mockApaterBackup,
374373
getCoordinatorVersion: () => {
375374
return {type: 'zStack', meta: {version: 1}};
376375
},
@@ -1117,7 +1116,27 @@ describe('Controller', () => {
11171116

11181117
it('Change channel on start', async () => {
11191118
mockAdapterStart.mockReturnValueOnce('resumed');
1120-
mockAdapterGetNetworkParameters.mockReturnValueOnce({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 25});
1119+
mockAdapterGetNetworkParameters.mockReturnValueOnce({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 25, nwkUpdateID: 0});
1120+
// @ts-expect-error private
1121+
const changeChannelSpy = vi.spyOn(controller, 'changeChannel');
1122+
await controller.start();
1123+
expect(mockAdapterGetNetworkParameters).toHaveBeenCalledTimes(1);
1124+
const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NWK_UPDATE_REQUEST, [15], 0xfe, undefined, 1, undefined);
1125+
expect(mockAdapterSendZdo).toHaveBeenCalledWith(
1126+
ZSpec.BLANK_EUI64,
1127+
ZSpec.BroadcastAddress.SLEEPY,
1128+
Zdo.ClusterId.NWK_UPDATE_REQUEST,
1129+
zdoPayload,
1130+
true,
1131+
);
1132+
mockAdapterGetNetworkParameters.mockReturnValueOnce({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 15, nwkUpdateID: 1});
1133+
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00', nwkUpdateID: 1});
1134+
expect(changeChannelSpy).toHaveBeenCalledTimes(1);
1135+
});
1136+
1137+
it('Change channel on start when nwkUpdateID is 0xff', async () => {
1138+
mockAdapterStart.mockReturnValueOnce('resumed');
1139+
mockAdapterGetNetworkParameters.mockReturnValueOnce({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 25, nwkUpdateID: 0xff});
11211140
// @ts-expect-error private
11221141
const changeChannelSpy = vi.spyOn(controller, 'changeChannel');
11231142
await controller.start();
@@ -1130,7 +1149,7 @@ describe('Controller', () => {
11301149
zdoPayload,
11311150
true,
11321151
);
1133-
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00'});
1152+
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00', nwkUpdateID: 0});
11341153
expect(changeChannelSpy).toHaveBeenCalledTimes(1);
11351154
});
11361155

@@ -1150,9 +1169,9 @@ describe('Controller', () => {
11501169

11511170
it('Get network parameters', async () => {
11521171
await controller.start();
1153-
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00'});
1172+
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00', nwkUpdateID: 0});
11541173
// cached
1155-
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00'});
1174+
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00', nwkUpdateID: 0});
11561175
expect(mockAdapterGetNetworkParameters).toHaveBeenCalledTimes(1);
11571176
});
11581177

0 commit comments

Comments
 (0)