Skip to content

Commit

Permalink
fix: Fix some devices not moving to new channel after channel change (#…
Browse files Browse the repository at this point in the history
…1280)

Co-authored-by: 郑泽涛 <1050713479@qq.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
  • Loading branch information
3 people authored Feb 24, 2025
1 parent 76d3de1 commit fc7a782
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 51 deletions.
4 changes: 4 additions & 0 deletions src/adapter/deconz/adapter/deconzAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,11 +536,15 @@ export class DeconzAdapter extends Adapter {
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);
// For some reason, reading NWK_UPDATE_ID always returns `null` (tested with `0x26780700` on Conbee II)
// 0x24 was taken from https://github.com/zigpy/zigpy-deconz/blob/70910bc6a63e607332b4f12754ba470651eb878c/zigpy_deconz/api.py#L152
// const nwkUpdateId = await this.driver.readParameterRequest(0x24 /*PARAM.PARAM.Network.NWK_UPDATE_ID*/);

return {
panID: panid as number,
extendedPanID: expanid as string, // read as `0x...`
channel: channel as number,
nwkUpdateID: 0 as number,
};
} catch (error) {
const msg = 'get network parameters Error:' + error;
Expand Down
6 changes: 5 additions & 1 deletion src/adapter/ember/adapter/emberAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,7 @@ export class EmberAdapter extends Adapter {
Array.from(backup!.networkOptions.extendedPanId),
backup!.logicalChannel,
backup!.ezsp!.hashed_tclk!, // valid from getStoredBackup
backup!.networkUpdateId,
);

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

result = 'reset';
Expand Down Expand Up @@ -1041,6 +1043,7 @@ export class EmberAdapter extends Adapter {
extendedPanId: ExtendedPanId,
radioChannel: number,
tcLinkKey: Buffer,
nwkUpdateId: number,
): Promise<void> {
const state: EmberInitialSecurityState = {
bitmask:
Expand Down Expand Up @@ -1100,7 +1103,7 @@ export class EmberAdapter extends Adapter {
radioChannel,
joinMethod: EmberJoinMethod.MAC_ASSOCIATION,
nwkManagerId: ZSpec.COORDINATOR_ADDRESS,
nwkUpdateId: 0,
nwkUpdateId,
channels: ZSpec.ALL_802_15_4_CHANNELS_MASK,
};

Expand Down Expand Up @@ -1678,6 +1681,7 @@ export class EmberAdapter extends Adapter {
panID,
extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(extendedPanID)),
channel,
nwkUpdateID: this.networkCache.parameters.nwkUpdateId,
};
});
}
Expand Down
1 change: 1 addition & 0 deletions src/adapter/ezsp/adapter/ezspAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/adapter/tstype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ export interface NetworkParameters {
panID: number;
extendedPanID: string; // `0x${string}` same as IEEE address
channel: number;
nwkUpdateID: number;
}
7 changes: 7 additions & 0 deletions src/adapter/z-stack/adapter/zStackAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,13 @@ export class ZStackAdapter extends Adapter {
panID: result.payload.panid as number,
extendedPanID: result.payload.extendedpanid as string, // read as IEEEADDR, so `0x${string}`
channel: result.payload.channel as number,
/**
* Return a dummy nwkUpdateId of 0, the nwkUpdateId is used when changing channels however the
* zstack API does not allow to set this value. Instead it automatically increments the nwkUpdateId
* based on the value in the NIB.
* https://github.com/Koenkk/zigbee-herdsman/pull/1280#discussion_r1947815987
*/
nwkUpdateID: 0,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/adapter/zboss/adapter/zbossAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export class ZBOSSAdapter extends Adapter {
panID,
extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(extendedPanID)),
channel,
nwkUpdateID: 0,
};
});
}
Expand Down
1 change: 1 addition & 0 deletions src/adapter/zigate/adapter/zigateAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export class ZiGateAdapter extends Adapter {
panID: result.payload.PANID as number,
extendedPanID: result.payload.ExtPANID as string, // read as IEEEADDR, so `0x${string}`
channel: result.payload.Channel as number,
nwkUpdateID: 0 as number,
};
} catch (error) {
throw new Error(`Get network parameters failed ${error}`);
Expand Down
22 changes: 19 additions & 3 deletions src/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,11 @@ export class Controller extends events.EventEmitter<ControllerEventMap> {
const netParams = await this.getNetworkParameters();
const configuredChannel = this.options.network.channelList[0];
const adapterChannel = netParams.channel;
const nwkUpdateID = netParams.nwkUpdateID;

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);
}
}

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

// 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.
if (++nwkUpdateID > 0xff) {
nwkUpdateID = 0x00;
}

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);
Expand Down
2 changes: 2 additions & 0 deletions test/adapter/ember/emberAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand All @@ -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);
});
Expand Down
2 changes: 1 addition & 1 deletion test/adapter/z-stack/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3511,7 +3511,7 @@ describe('zstack-adapter', () => {
const result = await adapter.getNetworkParameters();
expect(mockZnpRequest).toHaveBeenCalledTimes(1);
expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'extNwkInfo', {});
expect(result).toStrictEqual({channel: 21, extendedPanID: '0x00124b0009d69f77', panID: 123});
expect(result).toStrictEqual({channel: 21, extendedPanID: '0x00124b0009d69f77', panID: 123, nwkUpdateID: 0});
});

it('Set interpan channel', async () => {
Expand Down
111 changes: 65 additions & 46 deletions test/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -51,11 +89,12 @@ const mockAdapterReset = vi.fn();
const mockAdapterStop = vi.fn();
const mockAdapterStart = vi.fn().mockReturnValue('resumed');
const mockAdapterGetCoordinatorIEEE = vi.fn().mockReturnValue('0x0000012300000000');
const mockAdapterGetNetworkParameters = vi.fn().mockReturnValue({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 15});
const mockAdapterGetNetworkParameters = vi.fn().mockReturnValue({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 15, nwkUpdateID: 0});
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()
Expand Down Expand Up @@ -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', () => ({
Expand All @@ -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}};
},
Expand Down Expand Up @@ -1117,7 +1116,27 @@ describe('Controller', () => {

it('Change channel on start', async () => {
mockAdapterStart.mockReturnValueOnce('resumed');
mockAdapterGetNetworkParameters.mockReturnValueOnce({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 25});
mockAdapterGetNetworkParameters.mockReturnValueOnce({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 25, nwkUpdateID: 0});
// @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, 1, undefined);
expect(mockAdapterSendZdo).toHaveBeenCalledWith(
ZSpec.BLANK_EUI64,
ZSpec.BroadcastAddress.SLEEPY,
Zdo.ClusterId.NWK_UPDATE_REQUEST,
zdoPayload,
true,
);
mockAdapterGetNetworkParameters.mockReturnValueOnce({panID: 1, extendedPanID: '0x64c5fd698daf0c00', channel: 15, nwkUpdateID: 1});
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00', nwkUpdateID: 1});
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();
Expand All @@ -1130,7 +1149,7 @@ describe('Controller', () => {
zdoPayload,
true,
);
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00'});
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00', nwkUpdateID: 0});
expect(changeChannelSpy).toHaveBeenCalledTimes(1);
});

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

it('Get network parameters', async () => {
await controller.start();
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00'});
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00', nwkUpdateID: 0});
// cached
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00'});
expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: '0x64c5fd698daf0c00', nwkUpdateID: 0});
expect(mockAdapterGetNetworkParameters).toHaveBeenCalledTimes(1);
});

Expand Down

0 comments on commit fc7a782

Please sign in to comment.