Skip to content

Commit 86d1a76

Browse files
NerivecKoenkk
authored andcommitted
fix: General cleanup (#1231)
* Use ZSpec utils to deal with `0x` EUI conversion * Remove some restrictions in mDNS discovery * Fix typing (used by ZHC OTA) * Fix typing in controller tests (no more errors) * Allow `socket://` adapter path (converted internally to `tcp://`)
1 parent c016153 commit 86d1a76

File tree

15 files changed

+116
-75
lines changed

15 files changed

+116
-75
lines changed

src/adapter/adapter.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,12 @@ abstract class Adapter extends events.EventEmitter<AdapterEventMap> {
6565
zboss: ZBOSSAdapter,
6666
};
6767

68-
const [adapter, path, baudRate] = await discoverAdapter(serialPortOptions.adapter, serialPortOptions.path);
68+
const [adapter, path] = await discoverAdapter(serialPortOptions.adapter, serialPortOptions.path);
6969

7070
if (adapterLookup[adapter]) {
7171
serialPortOptions.adapter = adapter;
7272
serialPortOptions.path = path;
7373

74-
if (baudRate !== undefined) {
75-
serialPortOptions.baudRate = baudRate;
76-
}
77-
7874
return new adapterLookup[adapter](networkOptions, serialPortOptions, backupPath, adapterOptions);
7975
} else {
8076
throw new Error(`Adapter '${adapter}' does not exists, possible options: ${Object.keys(adapterLookup).join(', ')}`);

src/adapter/adapterDiscovery.ts

+10-18
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ export async function findUSBAdapter(
415415
}
416416
}
417417

418-
export async function findmDNSAdapter(path: string): Promise<[adapter: Adapter, path: string, baudRate: number]> {
418+
export async function findmDNSAdapter(path: string): Promise<[adapter: Adapter, path: string]> {
419419
const mdnsDevice = path.substring(7);
420420

421421
if (mdnsDevice.length == 0) {
@@ -430,31 +430,23 @@ export async function findmDNSAdapter(path: string): Promise<[adapter: Adapter,
430430
return await new Promise((resolve, reject) => {
431431
bj.findOne({type: mdnsDevice}, mdnsTimeout, function (service: Service) {
432432
if (service) {
433-
if (service.txt?.radio_type && service.txt?.baud_rate && service.addresses && service.port) {
434-
const mdnsIp = service.addresses[0];
433+
if (service.txt?.radio_type && service.port) {
434+
const mdnsAddress = service.addresses?.[0] ?? service.host;
435435
const mdnsPort = service.port;
436436
const mdnsAdapter = (service.txt.radio_type == 'znp' ? 'zstack' : service.txt.radio_type) as Adapter;
437-
const mdnsBaud = parseInt(service.txt.baud_rate);
438437

439-
logger.info(`Coordinator Ip: ${mdnsIp}`, NS);
438+
logger.info(`Coordinator Address: ${mdnsAddress}`, NS);
440439
logger.info(`Coordinator Port: ${mdnsPort}`, NS);
441440
logger.info(`Coordinator Radio: ${mdnsAdapter}`, NS);
442-
logger.info(`Coordinator Baud: ${mdnsBaud}\n`, NS);
443441
bj.destroy();
444442

445-
path = `tcp://${mdnsIp}:${mdnsPort}`;
446-
const adapter = mdnsAdapter;
447-
const baudRate = mdnsBaud;
448-
449-
resolve([adapter, path, baudRate]);
443+
resolve([mdnsAdapter, `tcp://${mdnsAddress}:${mdnsPort}`]);
450444
} else {
451445
bj.destroy();
452446
reject(
453447
new Error(
454448
`Coordinator returned wrong Zeroconf format! The following values are expected:\n` +
455449
`txt.radio_type, got: ${service.txt?.radio_type}\n` +
456-
`txt.baud_rate, got: ${service.txt?.baud_rate}\n` +
457-
`address, got: ${service.addresses?.[0]}\n` +
458450
`port, got: ${service.port}`,
459451
),
460452
);
@@ -468,7 +460,7 @@ export async function findmDNSAdapter(path: string): Promise<[adapter: Adapter,
468460
}
469461

470462
export async function findTCPAdapter(path: string, adapter?: Adapter): Promise<[adapter: Adapter, path: string]> {
471-
const regex = /^tcp:\/\/(?:[0-9]{1,3}\.){3}[0-9]{1,3}:\d{1,5}$/gm;
463+
const regex = /^(tcp|socket):\/\/(?:[0-9]{1,3}\.){3}[0-9]{1,3}:\d{1,5}$/gm;
472464

473465
if (!regex.test(path)) {
474466
throw new Error(`Invalid TCP path, expected format: tcp://<host>:<port>`);
@@ -478,7 +470,8 @@ export async function findTCPAdapter(path: string, adapter?: Adapter): Promise<[
478470
throw new Error(`Cannot discover TCP adapters at this time. Specify valid 'adapter' and 'port' in your configuration.`);
479471
}
480472

481-
return [adapter, path];
473+
// always use `tcp://` format
474+
return [adapter, path.replace(/^socket/, 'tcp')];
482475
}
483476

484477
/**
@@ -494,13 +487,12 @@ export async function findTCPAdapter(path: string, adapter?: Adapter): Promise<[
494487
* - USB: Optional, limits the discovery to the specified path.
495488
* @returns adapter An adapter type supported by Z2M. While result is TS-typed, this should be validated against actual values before use.
496489
* @returns path Path to adapter.
497-
* @returns baudRate [optional] Discovered baud rate of the adapter. Valid only for mDNS discovery at the moment.
498490
*/
499-
export async function discoverAdapter(adapter?: Adapter, path?: string): Promise<[adapter: Adapter, path: string, baudRate?: number | undefined]> {
491+
export async function discoverAdapter(adapter?: Adapter, path?: string): Promise<[adapter: Adapter, path: string]> {
500492
if (path) {
501493
if (path.startsWith('mdns://')) {
502494
return await findmDNSAdapter(path);
503-
} else if (path.startsWith('tcp://')) {
495+
} else if (path.startsWith('tcp://') || path.startsWith('socket://')) {
504496
return await findTCPAdapter(path, adapter);
505497
} else if (adapter) {
506498
try {

src/adapter/ember/adapter/emberAdapter.ts

+6-10
Original file line numberDiff line numberDiff line change
@@ -963,16 +963,12 @@ export class EmberAdapter extends Adapter {
963963
logger.info(`[INIT TC] Forming from backup.`, NS);
964964
// `backup` valid in this `action` path (not detected by TS)
965965
/* istanbul ignore next */
966-
const keyList: LinkKeyBackupData[] = backup!.devices.map((device) => {
967-
const octets = Array.from(device.ieeeAddress.reverse());
968-
969-
return {
970-
deviceEui64: `0x${octets.map((octet) => octet.toString(16).padStart(2, '0')).join('')}`,
971-
key: {contents: device.linkKey!.key},
972-
outgoingFrameCounter: device.linkKey!.txCounter,
973-
incomingFrameCounter: device.linkKey!.rxCounter,
974-
};
975-
});
966+
const keyList: LinkKeyBackupData[] = backup!.devices.map((device) => ({
967+
deviceEui64: ZSpec.Utils.eui64BEBufferToHex(device.ieeeAddress),
968+
key: {contents: device.linkKey!.key},
969+
outgoingFrameCounter: device.linkKey!.txCounter,
970+
incomingFrameCounter: device.linkKey!.rxCounter,
971+
}));
976972

977973
// before forming
978974
await this.importLinkKeys(keyList);

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as fs from 'fs';
44
import * as Models from '../../../models';
55
import {BackupUtils} from '../../../utils';
66
import {logger} from '../../../utils/logger';
7-
import {NULL_NODE_ID} from '../../../zspec';
7+
import {NULL_NODE_ID, Utils as ZSpecUtils} from '../../../zspec';
88
import {NvItemsIds, NvSystemIds} from '../constants/common';
99
import * as Structs from '../structs';
1010
import {AddressManagerUser, SecurityManagerAuthenticationOption} from '../structs';
@@ -232,10 +232,10 @@ export class AdapterBackup {
232232
const missing = oldBackup.devices.filter(
233233
(d) =>
234234
d.linkKey &&
235-
ieeeAddressesInDatabase.includes(`0x${d.ieeeAddress.toString('hex')}`) &&
235+
ieeeAddressesInDatabase.includes(ZSpecUtils.eui64BEBufferToHex(d.ieeeAddress)) &&
236236
!backup.devices.find((dd) => d.ieeeAddress.equals(dd.ieeeAddress)),
237237
);
238-
const missingStr = missing.map((d) => `0x${d.ieeeAddress.toString('hex')}`).join(', ');
238+
const missingStr = missing.map((d) => ZSpecUtils.eui64BEBufferToHex(d.ieeeAddress)).join(', ');
239239
logger.debug(
240240
`Following devices with link key are missing from new backup but present in old backup and database, ` +
241241
`adding them back: ${missingStr}`,

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

+1-11
Original file line numberDiff line numberDiff line change
@@ -1129,17 +1129,7 @@ class ZStackAdapter extends Adapter {
11291129
}
11301130

11311131
private toAddressString(address: number | string): string {
1132-
if (typeof address === 'number') {
1133-
let addressString = address.toString(16);
1134-
1135-
for (let i = addressString.length; i < 16; i++) {
1136-
addressString = '0' + addressString;
1137-
}
1138-
1139-
return `0x${addressString}`;
1140-
} else {
1141-
return address.toString();
1142-
}
1132+
return typeof address === 'number' ? `0x${address.toString(16).padStart(16, '0')}` : address.toString();
11431133
}
11441134

11451135
private waitressTimeoutFormatter(matcher: WaitressMatcher, timeout: number): string {

src/adapter/zigate/driver/buffaloZiGate.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* istanbul ignore file */
22

33
import {Buffalo} from '../../../buffalo';
4+
import {Utils as ZSpecUtils} from '../../../zspec';
45
import {EUI64} from '../../../zspec/tstypes';
56
import {BuffaloZclOptions} from '../../../zspec/zcl/definition/tstype';
67
import {getMacCapFlags} from '../../../zspec/zdo/utils';
@@ -182,8 +183,9 @@ class BuffaloZiGate extends Buffalo {
182183
}
183184

184185
public readIeeeAddrBE(): EUI64 {
185-
return `0x${this.readBuffer(8).toString('hex')}`;
186+
return ZSpecUtils.eui64BEBufferToHex(this.readBuffer(8));
186187
}
188+
187189
public writeIeeeAddrBE(value: string /*TODO: EUI64*/): void {
188190
this.writeUInt32BE(parseInt(value.slice(2, 10), 16));
189191
this.writeUInt32BE(parseInt(value.slice(10), 16));

src/buffalo/buffalo.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import {EUI64} from '../zspec/tstypes';
1+
import type {EUI64} from '../zspec/tstypes';
2+
3+
import {Utils as ZSpecUtils} from '../zspec';
24

35
class Buffalo {
46
protected position: number;
@@ -238,7 +240,7 @@ class Buffalo {
238240
}
239241

240242
public readIeeeAddr(): EUI64 {
241-
return `0x${Buffer.from(this.readBuffer(8)).reverse().toString('hex')}`;
243+
return ZSpecUtils.eui64LEBufferToHex(this.readBuffer(8));
242244
}
243245

244246
public writeBuffer(values: Buffer | number[], length: number): void {

src/controller/controller.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -387,10 +387,10 @@ class Controller extends events.EventEmitter<ControllerEventMap> {
387387
public async coordinatorCheck(): Promise<{missingRouters: Device[]}> {
388388
if (await this.adapter.supportsBackup()) {
389389
const backup = await this.adapter.backup(this.getDeviceIeeeAddresses());
390-
const devicesInBackup = backup.devices.map((d) => `0x${d.ieeeAddress.toString('hex')}`);
390+
const devicesInBackup = backup.devices.map((d) => ZSpec.Utils.eui64BEBufferToHex(d.ieeeAddress));
391391
const missingRouters = [];
392392

393-
for (const device of this.getDevicesIterator((d) => d.type === 'Router' && !devicesInBackup.includes(d.ieeeAddr))) {
393+
for (const device of this.getDevicesIterator((d) => d.type === 'Router' && !devicesInBackup.includes(d.ieeeAddr as EUI64))) {
394394
missingRouters.push(device);
395395
}
396396

@@ -640,7 +640,7 @@ class Controller extends events.EventEmitter<ControllerEventMap> {
640640

641641
// Green power devices don't have an ieeeAddr, the sourceID is unique and static so use this.
642642
let ieeeAddr = payload.sourceID.toString(16);
643-
ieeeAddr = `0x${'0'.repeat(16 - ieeeAddr.length)}${ieeeAddr}`;
643+
ieeeAddr = `0x${ieeeAddr.padStart(16, '0')}`;
644644

645645
// Green power devices dont' have a modelID, create a modelID based on the deviceID (=type)
646646
const modelID = `GreenPower_${payload.deviceID}`;

src/controller/model/endpoint.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,7 @@ class Endpoint extends Entity {
788788
public waitForCommand(
789789
clusterKey: number | string,
790790
commandKey: number | string,
791-
transactionSequenceNumber: number,
791+
transactionSequenceNumber: number | undefined,
792792
timeout: number,
793793
): {promise: Promise<{header: Zcl.Header; payload: KeyValue}>; cancel: () => void} {
794794
const device = this.getDevice();

src/zspec/utils.ts

+14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type {EUI64} from './tstypes';
2+
13
import {ALL_802_15_4_CHANNELS} from './consts';
24
import {BroadcastAddress} from './enums';
35

@@ -35,3 +37,15 @@ export const isBroadcastAddress = (address: number): boolean => {
3537
address === BroadcastAddress.LOW_POWER_ROUTERS
3638
);
3739
};
40+
41+
/**
42+
* Represent a little endian buffer in `0x...` form
43+
*
44+
* NOTE: the buffer is always copied to avoid reversal in reference
45+
*/
46+
export const eui64LEBufferToHex = (eui64LEBuf: Buffer): EUI64 => `0x${Buffer.from(eui64LEBuf).reverse().toString('hex')}`;
47+
48+
/**
49+
* Represent a big endian buffer in `0x...` form
50+
*/
51+
export const eui64BEBufferToHex = (eui64BEBuf: Buffer): EUI64 => `0x${eui64BEBuf.toString('hex')}`;

test/adapter/adapter.test.ts

+44-11
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,21 @@ const mockBonjourResult = jest.fn().mockImplementation((type) => ({
2626
name: 'Mock Adapter',
2727
type: `${type}_mdns`,
2828
port: '1122',
29+
host: 'mock_adapter.local',
2930
addresses: ['192.168.1.123'],
3031
txt: {
3132
radio_type: `${type}`,
32-
baud_rate: 115200,
3333
},
3434
}));
35-
const mockBonjourFindOne = jest.fn().mockImplementation((opts: BrowserConfig | null, timeout: number, callback?: CallableFunction) => {
35+
const mockBonjourFindOne = jest.fn((opts: BrowserConfig | null, timeout: number, callback?: CallableFunction) => {
3636
if (callback) {
3737
callback(mockBonjourResult(opts?.type));
3838
}
3939
});
4040
const mockBonjourDestroy = jest.fn();
4141

4242
jest.mock('bonjour-service', () => ({
43-
Bonjour: jest.fn().mockImplementation(() => ({
43+
Bonjour: jest.fn(() => ({
4444
findOne: mockBonjourFindOne,
4545
destroy: mockBonjourDestroy,
4646
})),
@@ -71,7 +71,6 @@ describe('Adapter', () => {
7171
expect(adapter).toBeInstanceOf(adapterCls);
7272
// @ts-expect-error protected
7373
expect(adapter.serialPortOptions).toStrictEqual({
74-
baudRate: 115200,
7574
path: 'tcp://192.168.1.123:1122',
7675
adapter: name,
7776
});
@@ -83,12 +82,31 @@ describe('Adapter', () => {
8382
expect(adapter).toBeInstanceOf(ZStackAdapter);
8483
// @ts-expect-error protected
8584
expect(adapter.serialPortOptions).toStrictEqual({
86-
baudRate: 115200,
8785
path: 'tcp://192.168.1.123:1122',
8886
adapter: 'zstack',
8987
});
9088
});
9189

90+
it('falls back to host if no addresses', async () => {
91+
mockBonjourResult.mockReturnValueOnce({
92+
name: 'Mock Adapter',
93+
type: `my_adapter_mdns`,
94+
port: '1122',
95+
host: 'mock_adapter.local',
96+
txt: {
97+
radio_type: `zstack`,
98+
},
99+
});
100+
const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: `mdns://zstack`}, 'test.db.backup', {disableLED: false});
101+
102+
expect(adapter).toBeInstanceOf(ZStackAdapter);
103+
// @ts-expect-error protected
104+
expect(adapter.serialPortOptions).toStrictEqual({
105+
path: 'tcp://mock_adapter.local:1122',
106+
adapter: `zstack`,
107+
});
108+
});
109+
92110
it('times out', async () => {
93111
mockBonjourResult.mockReturnValueOnce(null);
94112
const fakeAdapterName = 'mdns_test_device';
@@ -109,10 +127,10 @@ describe('Adapter', () => {
109127
name: 'Mock Adapter',
110128
type: `my_adapter_mdns`,
111129
port: '1122',
130+
host: 'my_adapter.local',
112131
addresses: ['192.168.1.123'],
113132
txt: {
114133
radio_type: undefined,
115-
baud_rate: 115200,
116134
},
117135
});
118136

@@ -121,15 +139,13 @@ describe('Adapter', () => {
121139
}).rejects.toThrow(
122140
`Coordinator returned wrong Zeroconf format! The following values are expected:\n` +
123141
`txt.radio_type, got: undefined\n` +
124-
`txt.baud_rate, got: 115200\n` +
125-
`address, got: 192.168.1.123\n` +
126142
`port, got: 1122`,
127143
);
128144
});
129145
});
130146

131147
describe('TCP discovery', () => {
132-
it('returns config', async () => {
148+
it('returns config with tcp path', async () => {
133149
const adapter = await Adapter.create(
134150
{panID: 0x1a62, channelList: [11]},
135151
{path: `tcp://192.168.1.321:3456`, adapter: `zstack`},
@@ -144,6 +160,21 @@ describe('Adapter', () => {
144160
});
145161
});
146162

163+
it('returns config with socket path', async () => {
164+
const adapter = await Adapter.create(
165+
{panID: 0x1a62, channelList: [11]},
166+
{path: `socket://192.168.1.321:3456`, adapter: `zstack`},
167+
'test.db.backup',
168+
{disableLED: false},
169+
);
170+
171+
// @ts-expect-error protected
172+
expect(adapter.serialPortOptions).toStrictEqual({
173+
path: `tcp://192.168.1.321:3456`,
174+
adapter: `zstack`,
175+
});
176+
});
177+
147178
it('invalid path', async () => {
148179
expect(async () => {
149180
await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: `tcp://192168.1.321:3456`, adapter: `zstack`}, 'test.db.backup', {
@@ -177,24 +208,26 @@ describe('Adapter', () => {
177208
it('detects each adapter', async () => {
178209
listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]);
179210

180-
let adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false});
211+
let adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {baudRate: 57600}, 'test.db.backup', {disableLED: false});
181212

182213
expect(adapter).toBeInstanceOf(DeconzAdapter);
183214
// @ts-expect-error protected
184215
expect(adapter.serialPortOptions).toStrictEqual({
185216
path: DECONZ_CONBEE_II.path,
186217
adapter: 'deconz',
218+
baudRate: 57600,
187219
});
188220

189221
listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]);
190222

191-
adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false});
223+
adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {baudRate: 115200}, 'test.db.backup', {disableLED: false});
192224

193225
expect(adapter).toBeInstanceOf(EmberAdapter);
194226
// @ts-expect-error protected
195227
expect(adapter.serialPortOptions).toStrictEqual({
196228
path: EMBER_ZBDONGLE_E.path,
197229
adapter: 'ember',
230+
baudRate: 115200,
198231
});
199232

200233
listSpy.mockReturnValueOnce([ZSTACK_CC2538]);

0 commit comments

Comments
 (0)