Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Improved adapter discovery. #1197

Merged
merged 13 commits into from
Oct 11, 2024
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