Skip to content

Commit

Permalink
feat!: Improved adapter discovery (#1197)
Browse files Browse the repository at this point in the history
* feat: Improved adapter discovery.

* Fix.

* Update + tests.

* Add alt regex matching on `pnpId`.

* Feedback.

* Lower log level on specified config matching fail.

* Improve matching logic to avoid false positive.

* Restrict search by config as much as possible. Reorganize tests, add more coverage. Cleanup.

* More fingerprints

* Add fingerprint for SMLight slzb-07mg24

* Add additional fingerprints + tests

* Better matching with scoring.

* Remove `auto` from Adapter type.

---------

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
  • Loading branch information
Nerivec and Koenkk committed Dec 1, 2024
1 parent 818435a commit 1d35a14
Show file tree
Hide file tree
Showing 20 changed files with 1,463 additions and 861 deletions.
122 changes: 9 additions & 113 deletions src/adapter/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import events from 'events';

import Bonjour, {Service} from 'bonjour-service';

import * as Models from '../models';
import {logger} from '../utils/logger';
import {BroadcastAddress} from '../zspec/enums';
import * as Zcl from '../zspec/zcl';
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';

const NS = 'zh:adapter';

interface AdapterEventMap {
deviceJoined: [payload: AdapterEvents.DeviceJoinedPayload];
zclPayload: [payload: AdapterEvents.ZclPayload];
Expand Down Expand Up @@ -60,15 +56,6 @@ abstract class Adapter extends events.EventEmitter<AdapterEventMap> {
const {EZSPAdapter} = await import('./ezsp/adapter');
const {EmberAdapter} = await import('./ember/adapter');
const {ZBOSSAdapter} = await import('./zboss/adapter');
type AdapterImplementation =
| typeof ZStackAdapter
| typeof DeconzAdapter
| typeof ZiGateAdapter
| typeof EZSPAdapter
| typeof EmberAdapter
| typeof ZBOSSAdapter;

let adapters: AdapterImplementation[];
const adapterLookup = {
zstack: ZStackAdapter,
deconz: DeconzAdapter,
Expand All @@ -78,111 +65,20 @@ abstract class Adapter extends events.EventEmitter<AdapterEventMap> {
zboss: ZBOSSAdapter,
};

if (serialPortOptions.adapter && serialPortOptions.adapter !== 'auto') {
if (adapterLookup[serialPortOptions.adapter]) {
adapters = [adapterLookup[serialPortOptions.adapter]];
} else {
throw new Error(`Adapter '${serialPortOptions.adapter}' does not exists, possible options: ${Object.keys(adapterLookup).join(', ')}`);
}
} else {
adapters = Object.values(adapterLookup);
}

// Use ZStackAdapter by default
let adapter: AdapterImplementation = adapters[0];

if (!serialPortOptions.path) {
logger.debug('No path provided, auto detecting path', NS);
for (const candidate of adapters) {
const path = await candidate.autoDetectPath();
if (path) {
logger.debug(`Auto detected path '${path}' from adapter '${candidate.name}'`, NS);
serialPortOptions.path = path;
adapter = candidate;
break;
}
}
const [adapter, path, baudRate] = await discoverAdapter(serialPortOptions.adapter, serialPortOptions.path);

if (!serialPortOptions.path) {
throw new Error('No path provided and failed to auto detect path');
}
} else if (serialPortOptions.path.startsWith('mdns://')) {
const mdnsDevice = serialPortOptions.path.substring(7);
if (adapterLookup[adapter]) {
serialPortOptions.adapter = adapter;
serialPortOptions.path = path;

if (mdnsDevice.length == 0) {
throw new Error(`No mdns device specified. You must specify the coordinator mdns service type after mdns://, e.g. mdns://my-adapter`);
if (baudRate !== undefined) {
serialPortOptions.baudRate = baudRate;
}

const bj = new Bonjour();
const mdnsTimeout = 2000; // timeout for mdns scan

logger.info(`Starting mdns discovery for coordinator: ${mdnsDevice}`, NS);

await new Promise((resolve, reject) => {
bj.findOne({type: mdnsDevice}, mdnsTimeout, function (service: Service) {
if (service) {
if (service.txt?.radio_type && service.txt?.baud_rate && service.addresses && service.port) {
const mdnsIp = service.addresses[0];
const mdnsPort = service.port;
const mdnsAdapter = (
service.txt.radio_type == 'znp' ? 'zstack' : service.txt.radio_type
) as TsType.SerialPortOptions['adapter'];
const mdnsBaud = parseInt(service.txt.baud_rate);

logger.info(`Coordinator Ip: ${mdnsIp}`, NS);
logger.info(`Coordinator Port: ${mdnsPort}`, NS);
logger.info(`Coordinator Radio: ${mdnsAdapter}`, NS);
logger.info(`Coordinator Baud: ${mdnsBaud}\n`, NS);
bj.destroy();

serialPortOptions.path = `tcp://${mdnsIp}:${mdnsPort}`;
serialPortOptions.adapter = mdnsAdapter;
serialPortOptions.baudRate = mdnsBaud;

if (
serialPortOptions.adapter &&
serialPortOptions.adapter !== 'auto' &&
adapterLookup[serialPortOptions.adapter] !== undefined
) {
adapter = adapterLookup[serialPortOptions.adapter];
resolve(new adapter(networkOptions, serialPortOptions, backupPath, adapterOptions));
} else {
reject(new Error(`Adapter ${serialPortOptions.adapter} is not supported.`));
}
} else {
bj.destroy();
reject(
new Error(
`Coordinator returned wrong Zeroconf format! The following values are expected:\n` +
`txt.radio_type, got: ${service.txt?.radio_type}\n` +
`txt.baud_rate, got: ${service.txt?.baud_rate}\n` +
`address, got: ${service.addresses?.[0]}\n` +
`port, got: ${service.port}`,
),
);
}
} else {
bj.destroy();
reject(new Error(`Coordinator [${mdnsDevice}] not found after timeout of ${mdnsTimeout}ms!`));
}
});
});
return new adapterLookup[adapter](networkOptions, serialPortOptions, backupPath, adapterOptions);
} else {
try {
// Determine adapter to use
for (const candidate of adapters) {
if (await candidate.isValidPath(serialPortOptions.path)) {
logger.debug(`Path '${serialPortOptions.path}' is valid for '${candidate.name}'`, NS);
adapter = candidate;
break;
}
}
} catch (error) {
logger.debug(`Failed to validate path: '${error}'`, NS);
}
throw new Error(`Adapter '${adapter}' does not exists, possible options: ${Object.keys(adapterLookup).join(', ')}`);
}

return new adapter(networkOptions, serialPortOptions, backupPath, adapterOptions);
}

public abstract start(): Promise<TsType.StartResult>;
Expand Down
Loading

0 comments on commit 1d35a14

Please sign in to comment.