From bd1da4513face56f9146ad8d570fbedb49f88c4b Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sun, 22 Sep 2024 22:02:51 +0200 Subject: [PATCH 01/13] feat: Improved adapter discovery. --- src/adapter/adapterDiscovery.ts | 373 ++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 src/adapter/adapterDiscovery.ts diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts new file mode 100644 index 0000000000..ec95725d39 --- /dev/null +++ b/src/adapter/adapterDiscovery.ts @@ -0,0 +1,373 @@ +import assert from 'assert'; +import {platform} from 'os'; + +import {PortInfo} from '@serialport/bindings-cpp'; +import Bonjour, {Service} from 'bonjour-service'; + +import {logger} from '../utils/logger'; +import {SerialPort} from './serialPort'; +import {SerialPortOptions} from './tstype'; + +const NS = 'zh:adapter:discovery'; + +type Adapter = NonNullable; +type DiscoverableUSBAdapter = 'deconz' | 'ember' | 'zstack' | 'zboss' | 'zigate'; +type USBFootprint = { + vendorId: string; + productId: string; + manufacturer?: string; + pathRegex?: string; +}; + +/** + * @see https://serialport.io/docs/api-bindings-cpp#list + * + * On Windows, there are occurrences where `manufacturer` is replaced by the OS driver. Example: `ITEAD` => `wch.cn`. + * + * In virtualized environments, the passthrough mechanism can affect the `path`. + * Example: + * Linux: /dev/serial/by-id/usb-ITEAD_SONOFF_Zigbee_3.0_USB_Dongle_Plus_V2_20240122184111-if00 + * Windows host => Linux guest: /dev/serial/by-id/usb-1a86_USB_Single_Serial_54DD002111-if00 + * + * XXX: vendorId `10c4` + productId `ea60` is a problem on Windows since can't match `path` and possibly can't match `manufacturer` to refine properly + */ +const USB_FOOTPRINTS: Record = { + deconz: [ + { + // Conbee II + vendorId: '1cf1', + productId: '0030', + manufacturer: 'dresden elektronik ingenieurtechnik GmbH', + // /dev/serial/by-id/usb-dresden_elektronik_ingenieurtechnik_GmbH_ConBee_II_DE2132111-if00 + pathRegex: '.*conbee.*', + }, + { + // Conbee III + vendorId: '0403', + productId: '6015', + manufacturer: 'dresden elektronik ingenieurtechnik GmbH', + // /dev/serial/by-id/usb-dresden_elektronik_ConBee_III_DE03188111-if00-port0 + pathRegex: '.*conbee.*', + }, + ], + ember: [ + // { + // // TODO: Easyiot ZB-GW04 (v1.1) + // vendorId: '', + // productId: '', + // manufacturer: '', + // pathRegex: '.*.*', + // }, + // { + // // TODO: Easyiot ZB-GW04 (v1.2) + // vendorId: '1a86', + // productId: '', + // manufacturer: '', + // // /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 + // pathRegex: '.*.*', + // }, + { + // Home Assistant SkyConnect + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'Nabu Casa', + // /dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_3abe54797c91ed118fc3cad13b20a111-if00-port0 + pathRegex: '.*Nabu_Casa_SkyConnect.*', + }, + // { + // // TODO: Home Assistant Yellow + // vendorId: '', + // productId: '', + // manufacturer: '', + // // /dev/ttyAMA1 + // pathRegex: '.*.*', + // }, + { + // SMLight slzb-07 + vendorId: '10c4', + productId: 'ea60', + // manufacturer: '', + // /dev/serial/by-id/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_a215650c853bec119a079e957a0af111-if00-port0 + pathRegex: '.*slzb-07.*', + }, + { + // Sonoff ZBDongle-E V2 + vendorId: '1a86', + productId: '55d4', + manufacturer: 'ITEAD', + // /dev/serial/by-id/usb-ITEAD_SONOFF_Zigbee_3.0_USB_Dongle_Plus_V2_20240122184111-if00 + pathRegex: '.*sonoff.*plus.*', + }, + // { + // // TODO: Z-station by z-wave.me (EFR32MG21A020F1024IM32) + // vendorId: '', + // productId: '', + // // manufacturer: '', + // // /dev/serial/by-id/usb-Silicon_Labs_CP2105_Dual_USB_to_UART_Bridge_Controller_012BA111-if01-port0 + // pathRegex: '.*CP2105.*', + // }, + ], + zstack: [ + { + // ZZH + vendorId: '0403', + productId: '6015', + manufacturer: 'Electrolama', + // pathRegex: '.*.*', + }, + { + // slae.sh cc2652rb + vendorId: '10c4', + productId: 'ea60', + // manufacturer: '', + pathRegex: '.*2652.*', + }, + { + // Sonoff ZBDongle-P (CC2652P) + vendorId: '10c4', + productId: 'ea60', + // manufacturer: '', + // /dev/serial/by-id/usb-Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus_0111-if00-port0 + // /dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_b8b49abd27a6ed11a280eba32981d111-if00-port0 + pathRegex: '.*sonoff.*plus.*', + }, + { + // CC2538 + vendorId: '0451', + productId: '16c8', + manufacturer: 'Texas Instruments', + // zStack30x: /dev/serial/by-id/usb-Texas_Instruments_CC2538_USB_CDC-if00 + pathRegex: '.*CC2538.*', + }, + { + // CC2531 + vendorId: '0451', + productId: '16a8', + manufacturer: 'Texas Instruments', + // /dev/serial/by-id/usb-Texas_Instruments_TI_CC2531_USB_CDC___0X00124B0018ED1111-if00 + pathRegex: '.*CC2531.*', + }, + { + // CC1352P_2 and CC26X2R1 + vendorId: '0451', + productId: 'bef3', + manufacturer: 'Texas Instruments', + // pathRegex: '.*.*', + }, + { + // SMLight slzb-07p7 + vendorId: '', + productId: '', + // manufacturer: '', + // /dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-07p7_be9faa0786e1ea11bd68dc2d9a583cc7-if00-port0 + pathRegex: '.*SLZB-07p7.*', + }, + { + // TubesZB ? + vendorId: '10c4', + productId: 'ea60', + // manufacturer: '', + pathRegex: '.*tubeszb.*', + }, + { + // TubesZB ? + vendorId: '1a86', + productId: '7523', + // manufacturer: '', + pathRegex: '.*tubeszb.*', + }, + { + // ZigStar + vendorId: '1a86', + productId: '7523', + // manufacturer: '', + pathRegex: '.*zigstar.*', + }, + ], + zboss: [ + { + // Nordic Zigbee NCP + vendorId: '2fe3', + productId: '0100', + manufacturer: 'ZEPHYR', + // pathRegex: '.*.*', + }, + ], + zigate: [ + { + // ZiGate PL2303HX (blue) + vendorId: '067b', + productId: '2303', + manufacturer: 'zigate_PL2303', + pathRegex: '.*zigate.*', + }, + { + // ZiGate CP2102 (red) + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'zigate_cp2102', + pathRegex: '.*zigate.*', + }, + { + // ZiGate+ V2 CDM_21228 + vendorId: '0403', + productId: '6015', + // manufacturer: '', + // /dev/serial/by-id/usb-FTDI_ZiGate_ZIGATE+-if00-port0 + pathRegex: '.*zigate.*', + }, + ], +}; + +function matchUSBFootprint(portInfo: PortInfo, isWindows: boolean, entries: USBFootprint[]): [PortInfo['path'], USBFootprint] | undefined { + if (!portInfo.vendorId || !portInfo.productId) { + // port info is missing essential information for proper matching, ignore it + return; + } + + for (const entry of entries) { + if ( + portInfo.vendorId.localeCompare(entry.vendorId, undefined, {sensitivity: 'base'}) === 0 && + portInfo.productId.localeCompare(entry.productId, undefined, {sensitivity: 'base'}) === 0 && + (!entry.manufacturer || + !portInfo.manufacturer || + portInfo.manufacturer.localeCompare(entry.manufacturer, undefined, {sensitivity: 'base'}) === 0 || + isWindows) && + (!entry.pathRegex || new RegExp(entry.pathRegex, 'i').test(portInfo.path) || isWindows) + ) { + return [portInfo.path, entry]; + } + } +} + +export async function findUSBAdapter(adapter?: Adapter, path?: string): Promise<[adapter: Adapter, path: PortInfo['path']] | undefined> { + assert(adapter !== 'auto' && adapter !== 'ezsp', `Cannot discover USB adapter for '${adapter}'.`); + + const isWindows = platform() === 'win32'; + + for (const portInfo of await SerialPort.list()) { + if (path && portInfo.path !== path) { + continue; + } + + if (adapter) { + const match = matchUSBFootprint(portInfo, isWindows, USB_FOOTPRINTS[adapter]); + + if (match) { + logger.info(`Matched adapter: ${JSON.stringify(portInfo)} => ${adapter}: ${JSON.stringify(match[1])}`, NS); + return [adapter, match[0]]; + } + + continue; + } + + for (const key in USB_FOOTPRINTS) { + const match = matchUSBFootprint(portInfo, isWindows, USB_FOOTPRINTS[key as DiscoverableUSBAdapter]!); + + if (match) { + logger.info(`Matched adapter: ${JSON.stringify(portInfo)} => ${key}: ${JSON.stringify(match[1])}`, NS); + return [key as Adapter, match[0]]; + } + } + } +} + +export async function findmDNSAdapter(path: string): Promise<[adapter: string, path: string, baudRate: number]> { + const mdnsDevice = path.substring(7); + + 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`); + } + + const bj = new Bonjour(); + const mdnsTimeout = 2000; // timeout for mdns scan + + logger.info(`Starting mdns discovery for coordinator: ${mdnsDevice}`, NS); + + return 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 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(); + + path = `tcp://${mdnsIp}:${mdnsPort}`; + const adapter = mdnsAdapter; + const baudRate = mdnsBaud; + + if (adapter && adapter !== 'auto') { + resolve([adapter, path, baudRate]); + } else { + reject(new Error(`Adapter ${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!`)); + } + }); + }); +} + +export async function findTCPAdapter(path: string, adapter?: Adapter): Promise<[adapter: string, path: string]> { + const regex = /^(?:tcp:\/\/)[\w.-]+[:][\d]+$/gm; + + if (!regex.test(path) || !adapter || adapter === 'auto') { + throw new Error(`Cannot discover TCP adapters at this time. Please specify valid 'adapter' and 'path' manually.`); + } + + return [adapter, path]; +} + +/** + * Discover adapter using mDNS, TCP or USB. + * + * @param adapter The adapter type. + * - mDNS: Unused. + * - TCP: Required, cannot discover at this time. + * - USB: Optional, limits the discovery to the specified adapter type. + * @param path The path to the adapter. + * - mDNS: Required, serves to initiate the discovery. + * - TCP: Required, cannot discover at this time. + * - USB: Optional, limits the discovery to the specified path. + * @returns adapter An adapter type supported by Z2M. While result is TS-typed, this should be validated against actual values before use. + * @returns path Path to adapter. + * @returns baudRate [optional] Discovered baud rate of the adapter. Valid only for mDNS discovery at the moment. + */ +export async function discoverAdapter(adapter?: Adapter, path?: string): Promise<[adapter: string, path: string, baudRate?: number | undefined]> { + if (path) { + if (path.startsWith('mdns://')) { + return await findmDNSAdapter(path); + } else if (path.startsWith('tcp://')) { + return await findTCPAdapter(path, adapter); + } + } + + // default to matching USB + const match = await findUSBAdapter(adapter === 'auto' ? undefined : adapter, path); + + if (!match) { + throw new Error(`Unable to find matching USB adapter.`); + } + + return match; +} From 04960ac4cca4dd0a703e66a3e8d1e7e0664d4ec0 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sun, 22 Sep 2024 22:06:32 +0200 Subject: [PATCH 02/13] Fix. --- src/adapter/adapterDiscovery.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index ec95725d39..37819e45e8 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -12,7 +12,7 @@ const NS = 'zh:adapter:discovery'; type Adapter = NonNullable; type DiscoverableUSBAdapter = 'deconz' | 'ember' | 'zstack' | 'zboss' | 'zigate'; -type USBFootprint = { +type USBFingerprint = { vendorId: string; productId: string; manufacturer?: string; @@ -31,7 +31,7 @@ type USBFootprint = { * * XXX: vendorId `10c4` + productId `ea60` is a problem on Windows since can't match `path` and possibly can't match `manufacturer` to refine properly */ -const USB_FOOTPRINTS: Record = { +const USB_FINGERPRINTS: Record = { deconz: [ { // Conbee II @@ -219,7 +219,7 @@ const USB_FOOTPRINTS: Record = { ], }; -function matchUSBFootprint(portInfo: PortInfo, isWindows: boolean, entries: USBFootprint[]): [PortInfo['path'], USBFootprint] | undefined { +function matchUSBFingerprint(portInfo: PortInfo, isWindows: boolean, entries: USBFingerprint[]): [PortInfo['path'], USBFingerprint] | undefined { if (!portInfo.vendorId || !portInfo.productId) { // port info is missing essential information for proper matching, ignore it return; @@ -251,7 +251,7 @@ export async function findUSBAdapter(adapter?: Adapter, path?: string): Promise< } if (adapter) { - const match = matchUSBFootprint(portInfo, isWindows, USB_FOOTPRINTS[adapter]); + const match = matchUSBFingerprint(portInfo, isWindows, USB_FINGERPRINTS[adapter]); if (match) { logger.info(`Matched adapter: ${JSON.stringify(portInfo)} => ${adapter}: ${JSON.stringify(match[1])}`, NS); @@ -261,8 +261,8 @@ export async function findUSBAdapter(adapter?: Adapter, path?: string): Promise< continue; } - for (const key in USB_FOOTPRINTS) { - const match = matchUSBFootprint(portInfo, isWindows, USB_FOOTPRINTS[key as DiscoverableUSBAdapter]!); + for (const key in USB_FINGERPRINTS) { + const match = matchUSBFingerprint(portInfo, isWindows, USB_FINGERPRINTS[key as DiscoverableUSBAdapter]!); if (match) { logger.info(`Matched adapter: ${JSON.stringify(portInfo)} => ${key}: ${JSON.stringify(match[1])}`, NS); From 24008cfa9b6669e1eef9589440ddcdb158b4e9ae Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Mon, 23 Sep 2024 04:06:48 +0200 Subject: [PATCH 03/13] Update + tests. --- src/adapter/adapter.ts | 122 +---- src/adapter/adapterDiscovery.ts | 113 +++-- src/adapter/deconz/adapter/deconzAdapter.ts | 8 - src/adapter/deconz/driver/driver.ts | 14 - src/adapter/ember/adapter/emberAdapter.ts | 34 +- src/adapter/ezsp/adapter/ezspAdapter.ts | 29 +- src/adapter/serialPortUtils.ts | 26 -- src/adapter/tstype.ts | 52 +-- src/adapter/z-stack/adapter/zStackAdapter.ts | 8 - src/adapter/z-stack/znp/znp.ts | 35 +- src/adapter/zboss/adapter/zbossAdapter.ts | 29 +- src/adapter/zigate/adapter/zigateAdapter.ts | 8 - src/adapter/zigate/driver/zigate.ts | 15 - src/utils/equalsPartial.ts | 8 - src/utils/index.ts | 3 +- test/adapter/adapter.test.ts | 456 +++++++++++++++++++ test/adapter/z-stack/adapter.test.ts | 14 - test/adapter/z-stack/znp.test.ts | 56 --- test/controller.test.ts | 443 +----------------- test/mockAdapters.ts | 29 ++ 20 files changed, 604 insertions(+), 898 deletions(-) delete mode 100644 src/adapter/serialPortUtils.ts delete mode 100644 src/utils/equalsPartial.ts create mode 100644 test/adapter/adapter.test.ts create mode 100644 test/mockAdapters.ts diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index 2f4b36498c..c273b592cd 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -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]; @@ -60,15 +56,6 @@ abstract class Adapter extends events.EventEmitter { 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, @@ -78,111 +65,20 @@ abstract class Adapter extends events.EventEmitter { 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; diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index 37819e45e8..3db8619583 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -1,24 +1,14 @@ -import assert from 'assert'; import {platform} from 'os'; import {PortInfo} from '@serialport/bindings-cpp'; -import Bonjour, {Service} from 'bonjour-service'; +import {Bonjour, Service} from 'bonjour-service'; import {logger} from '../utils/logger'; import {SerialPort} from './serialPort'; -import {SerialPortOptions} from './tstype'; +import {Adapter, DiscoverableUSBAdapter, USBAdapterFingerprint, ValidAdapter} from './tstype'; const NS = 'zh:adapter:discovery'; -type Adapter = NonNullable; -type DiscoverableUSBAdapter = 'deconz' | 'ember' | 'zstack' | 'zboss' | 'zigate'; -type USBFingerprint = { - vendorId: string; - productId: string; - manufacturer?: string; - pathRegex?: string; -}; - /** * @see https://serialport.io/docs/api-bindings-cpp#list * @@ -31,7 +21,7 @@ type USBFingerprint = { * * XXX: vendorId `10c4` + productId `ea60` is a problem on Windows since can't match `path` and possibly can't match `manufacturer` to refine properly */ -const USB_FINGERPRINTS: Record = { +const USB_FINGERPRINTS: Record = { deconz: [ { // Conbee II @@ -113,7 +103,7 @@ const USB_FINGERPRINTS: Record = { vendorId: '0403', productId: '6015', manufacturer: 'Electrolama', - // pathRegex: '.*.*', + pathRegex: '.*electrolame.*', // TODO }, { // slae.sh cc2652rb @@ -148,11 +138,18 @@ const USB_FINGERPRINTS: Record = { pathRegex: '.*CC2531.*', }, { - // CC1352P_2 and CC26X2R1 + // CC1352P_2 + vendorId: '0451', + productId: 'bef3', + manufacturer: 'Texas Instruments', + pathRegex: '.*CC1352P_2.*', // TODO + }, + { + // CC26X2R1 vendorId: '0451', productId: 'bef3', manufacturer: 'Texas Instruments', - // pathRegex: '.*.*', + pathRegex: '.*CC26X2R1.*', // TODO }, { // SMLight slzb-07p7 @@ -190,7 +187,7 @@ const USB_FINGERPRINTS: Record = { vendorId: '2fe3', productId: '0100', manufacturer: 'ZEPHYR', - // pathRegex: '.*.*', + pathRegex: '.*ZEPHYR.*', // TODO }, ], zigate: [ @@ -219,7 +216,25 @@ const USB_FINGERPRINTS: Record = { ], }; -function matchUSBFingerprint(portInfo: PortInfo, isWindows: boolean, entries: USBFingerprint[]): [PortInfo['path'], USBFingerprint] | undefined { +async function getSerialPortList(): Promise { + const portInfos = await SerialPort.list(); + + // TODO: can sorting be removed in favor of `path` regex matching? + + // CC1352P_2 and CC26X2R1 lists as 2 USB devices with same manufacturer, productId and vendorId + // one is the actual chip interface, other is the XDS110. + // The chip is always exposed on the first one after alphabetical sorting. + /* istanbul ignore next */ + portInfos.sort((a, b) => (a.path < b.path ? -1 : 1)); + + return portInfos; +} + +function matchUSBFingerprint( + portInfo: PortInfo, + isWindows: boolean, + entries: USBAdapterFingerprint[], +): [PortInfo['path'], USBAdapterFingerprint] | undefined { if (!portInfo.vendorId || !portInfo.productId) { // port info is missing essential information for proper matching, ignore it return; @@ -240,24 +255,32 @@ function matchUSBFingerprint(portInfo: PortInfo, isWindows: boolean, entries: US } } -export async function findUSBAdapter(adapter?: Adapter, path?: string): Promise<[adapter: Adapter, path: PortInfo['path']] | undefined> { - assert(adapter !== 'auto' && adapter !== 'ezsp', `Cannot discover USB adapter for '${adapter}'.`); - +export async function matchUSBAdapter(adapter: ValidAdapter, path: string): Promise { const isWindows = platform() === 'win32'; - for (const portInfo of await SerialPort.list()) { - if (path && portInfo.path !== path) { + for (const portInfo of await getSerialPortList()) { + /* istanbul ignore else */ + if (portInfo.path !== path) { continue; } - if (adapter) { - const match = matchUSBFingerprint(portInfo, isWindows, USB_FINGERPRINTS[adapter]); + const match = matchUSBFingerprint(portInfo, isWindows, USB_FINGERPRINTS[adapter === 'ezsp' ? 'ember' : adapter]); - if (match) { - logger.info(`Matched adapter: ${JSON.stringify(portInfo)} => ${adapter}: ${JSON.stringify(match[1])}`, NS); - return [adapter, match[0]]; - } + /* istanbul ignore else */ + if (match) { + logger.info(`Matched adapter: ${JSON.stringify(portInfo)} => ${adapter}: ${JSON.stringify(match[1])}`, NS); + return true; + } + } + + return false; +} + +export async function findUSBAdapter(path?: string): Promise<[adapter: DiscoverableUSBAdapter, path: PortInfo['path']] | undefined> { + const isWindows = platform() === 'win32'; + for (const portInfo of await getSerialPortList()) { + if (path && portInfo.path !== path) { continue; } @@ -266,13 +289,13 @@ export async function findUSBAdapter(adapter?: Adapter, path?: string): Promise< if (match) { logger.info(`Matched adapter: ${JSON.stringify(portInfo)} => ${key}: ${JSON.stringify(match[1])}`, NS); - return [key as Adapter, match[0]]; + return [key as DiscoverableUSBAdapter, match[0]]; } } } } -export async function findmDNSAdapter(path: string): Promise<[adapter: string, path: string, baudRate: number]> { +export async function findmDNSAdapter(path: string): Promise<[adapter: ValidAdapter, path: string, baudRate: number]> { const mdnsDevice = path.substring(7); if (mdnsDevice.length == 0) { @@ -328,10 +351,14 @@ export async function findmDNSAdapter(path: string): Promise<[adapter: string, p }); } -export async function findTCPAdapter(path: string, adapter?: Adapter): Promise<[adapter: string, path: string]> { - const regex = /^(?:tcp:\/\/)[\w.-]+[:][\d]+$/gm; +export async function findTCPAdapter(path: string, adapter?: Adapter): Promise<[adapter: ValidAdapter, path: string]> { + const regex = /^tcp:\/\/(?:[0-9]{1,3}\.){3}[0-9]{1,3}:\d{1,5}$/gm; + + if (!regex.test(path)) { + throw new Error(`Invalid TCP path, expected format: tcp://:`); + } - if (!regex.test(path) || !adapter || adapter === 'auto') { + if (!adapter || adapter === 'auto') { throw new Error(`Cannot discover TCP adapters at this time. Please specify valid 'adapter' and 'path' manually.`); } @@ -353,20 +380,32 @@ export async function findTCPAdapter(path: string, adapter?: Adapter): Promise<[ * @returns path Path to adapter. * @returns baudRate [optional] Discovered baud rate of the adapter. Valid only for mDNS discovery at the moment. */ -export async function discoverAdapter(adapter?: Adapter, path?: string): Promise<[adapter: string, path: string, baudRate?: number | undefined]> { +export async function discoverAdapter( + adapter?: Adapter, + path?: string, +): Promise<[adapter: ValidAdapter, path: string, baudRate?: number | undefined]> { if (path) { if (path.startsWith('mdns://')) { return await findmDNSAdapter(path); } else if (path.startsWith('tcp://')) { return await findTCPAdapter(path, adapter); + } else if (adapter && adapter !== 'auto') { + const matched = await matchUSBAdapter(adapter, path); + + /* istanbul ignore else */ + if (!matched) { + logger.error(`Unable to match USB adapter: ${adapter} | ${path}`, NS); + } + + return [adapter, path]; } } // default to matching USB - const match = await findUSBAdapter(adapter === 'auto' ? undefined : adapter, path); + const match = await findUSBAdapter(path); if (!match) { - throw new Error(`Unable to find matching USB adapter.`); + throw new Error(`Unable to find a valid USB adapter.`); } return match; diff --git a/src/adapter/deconz/adapter/deconzAdapter.ts b/src/adapter/deconz/adapter/deconzAdapter.ts index 559b8d3133..a59a65eb4c 100644 --- a/src/adapter/deconz/adapter/deconzAdapter.ts +++ b/src/adapter/deconz/adapter/deconzAdapter.ts @@ -76,14 +76,6 @@ class DeconzAdapter extends Adapter { }, 1000); } - public static async isValidPath(path: string): Promise { - return await Driver.isValidPath(path); - } - - public static async autoDetectPath(): Promise { - return await Driver.autoDetectPath(); - } - /** * Adapter methods */ diff --git a/src/adapter/deconz/driver/driver.ts b/src/adapter/deconz/driver/driver.ts index f5df769d7f..9e8017949e 100644 --- a/src/adapter/deconz/driver/driver.ts +++ b/src/adapter/deconz/driver/driver.ts @@ -8,7 +8,6 @@ import slip from 'slip'; import {logger} from '../../../utils/logger'; import {SerialPort} from '../../serialPort'; -import SerialPortUtils from '../../serialPortUtils'; import SocketPortUtils from '../../socketPortUtils'; import PARAM, {ApsDataRequest, parameterT, ReceivedDataResponse, Request} from './constants'; import {frameParserEvents} from './frameParser'; @@ -17,10 +16,6 @@ import Writer from './writer'; const NS = 'zh:deconz:driver'; -const autoDetectDefinitions = [ - {manufacturer: 'dresden elektronik ingenieurtechnik GmbH', vendorId: '1cf1', productId: '0030'}, // Conbee II -]; - const queue: Array = []; export const busyQueue: Array = []; const apsQueue: Array = []; @@ -190,15 +185,6 @@ class Driver extends events.EventEmitter { ); // query confirm and indication requests } - public static async isValidPath(path: string): Promise { - return SerialPortUtils.is(path, autoDetectDefinitions); - } - - public static async autoDetectPath(): Promise { - const paths = await SerialPortUtils.find(autoDetectDefinitions); - return paths.length > 0 ? paths[0] : undefined; - } - private onPortClose(): void { logger.debug('Port closed', NS); this.initialized = false; diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 42ef5f85bf..be348a20b7 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -6,7 +6,7 @@ import equals from 'fast-deep-equal/es6'; import {Adapter, TsType} from '../..'; import {Backup, UnifiedBackupStorage} from '../../../models'; -import {BackupUtils, Queue, RealpathSync, Wait} from '../../../utils'; +import {BackupUtils, Queue, Wait} from '../../../utils'; import {logger} from '../../../utils/logger'; import * as ZSpec from '../../../zspec'; import {EUI64, ExtendedPanId, NodeId, PanId} from '../../../zspec/tstypes'; @@ -14,8 +14,6 @@ import * as Zcl from '../../../zspec/zcl'; import * as Zdo from '../../../zspec/zdo'; import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; import {DeviceJoinedPayload, DeviceLeavePayload, ZclPayload} from '../../events'; -import SerialPortUtils from '../../serialPortUtils'; -import SocketPortUtils from '../../socketPortUtils'; import { EMBER_HIGH_RAM_CONCENTRATOR, EMBER_INSTALL_CODE_CRC_SIZE, @@ -111,14 +109,6 @@ enum NetworkInitAction { FORM_BACKUP, } -/** NOTE: Drivers can override `manufacturer`. Verify logic doesn't work in most cases anyway. */ -const autoDetectDefinitions = [ - /** NOTE: Manuf code "0x1321" for "Shenzhen Sonoff Technologies Co., Ltd." */ - {manufacturer: 'ITEAD', vendorId: '1a86', productId: '55d4'}, // Sonoff ZBDongle-E - /** NOTE: Manuf code "0x134B" for "Nabu Casa, Inc." */ - {manufacturer: 'Nabu Casa', vendorId: '10c4', productId: 'ea60'}, // Home Assistant SkyConnect -]; - /** * Application generated ZDO messages use sequence numbers 0-127, and the stack * uses sequence numbers 128-255. This simplifies life by eliminating the need @@ -1571,28 +1561,6 @@ export class EmberAdapter extends Adapter { //-- START Adapter implementation - /* istanbul ignore next */ - public static async isValidPath(path: string): Promise { - // For TCP paths we cannot get device information, therefore we cannot validate it. - if (SocketPortUtils.isTcpPath(path)) { - return false; - } - - try { - return await SerialPortUtils.is(RealpathSync(path), autoDetectDefinitions); - } catch (error) { - logger.debug(`Failed to determine if path is valid: '${error}'`, NS); - return false; - } - } - - /* istanbul ignore next */ - public static async autoDetectPath(): Promise { - const paths = await SerialPortUtils.find(autoDetectDefinitions); - paths.sort((a, b) => (a < b ? -1 : 1)); - return paths.length > 0 ? paths[0] : undefined; - } - public async start(): Promise { logger.info(`======== Ember Adapter Starting ========`, NS); const result = await this.initEzsp(); diff --git a/src/adapter/ezsp/adapter/ezspAdapter.ts b/src/adapter/ezsp/adapter/ezspAdapter.ts index 4ec74905b1..93528ac1da 100644 --- a/src/adapter/ezsp/adapter/ezspAdapter.ts +++ b/src/adapter/ezsp/adapter/ezspAdapter.ts @@ -3,7 +3,7 @@ import assert from 'assert'; import * as Models from '../../../models'; -import {Queue, RealpathSync, Wait, Waitress} from '../../../utils'; +import {Queue, Wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; import * as ZSpec from '../../../zspec'; import * as Zcl from '../../../zspec/zcl'; @@ -11,19 +11,12 @@ import * as Zdo from '../../../zspec/zdo'; import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; import Adapter from '../../adapter'; import {ZclPayload} from '../../events'; -import SerialPortUtils from '../../serialPortUtils'; -import SocketPortUtils from '../../socketPortUtils'; import {AdapterOptions, CoordinatorVersion, NetworkOptions, NetworkParameters, SerialPortOptions, StartResult} from '../../tstype'; import {Driver, EmberIncomingMessage} from '../driver'; import {EmberEUI64, EmberStatus} from '../driver/types'; const NS = 'zh:ezsp'; -const autoDetectDefinitions = [ - {manufacturer: 'ITEAD', vendorId: '1a86', productId: '55d4'}, // Sonoff ZBDongle-E - {manufacturer: 'Nabu Casa', vendorId: '10c4', productId: 'ea60'}, // Home Assistant SkyConnect -]; - interface WaitressMatcher { address?: number | string; endpoint: number; @@ -165,26 +158,6 @@ class EZSPAdapter extends Adapter { } } - public static async isValidPath(path: string): Promise { - // For TCP paths we cannot get device information, therefore we cannot validate it. - if (SocketPortUtils.isTcpPath(path)) { - return false; - } - - try { - return await SerialPortUtils.is(RealpathSync(path), autoDetectDefinitions); - } catch (error) { - logger.debug(`Failed to determine if path is valid: '${error}'`, NS); - return false; - } - } - - public static async autoDetectPath(): Promise { - const paths = await SerialPortUtils.find(autoDetectDefinitions); - paths.sort((a, b) => (a < b ? -1 : 1)); - return paths.length > 0 ? paths[0] : undefined; - } - public async getCoordinatorIEEE(): Promise { return `0x${this.driver.ieee.toString()}`; } diff --git a/src/adapter/serialPortUtils.ts b/src/adapter/serialPortUtils.ts deleted file mode 100644 index 1377cfa132..0000000000 --- a/src/adapter/serialPortUtils.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {EqualsPartial} from '../utils'; -import {SerialPort} from './serialPort'; - -interface PortInfoMatch { - manufacturer: string; - vendorId: string; - productId: string; -} - -async function find(matchers: PortInfoMatch[]): Promise { - let devices = await SerialPort.list(); - devices = devices.filter((device) => matchers.find((matcher) => EqualsPartial(device, matcher)) != null); - return devices.map((device) => device.path); -} - -async function is(path: string, matchers: PortInfoMatch[]): Promise { - const devices = await SerialPort.list(); - const device = devices.find((device) => device.path === path); - if (!device) { - return false; - } - - return matchers.find((matcher) => EqualsPartial(device, matcher)) != null; -} - -export default {is, find}; diff --git a/src/adapter/tstype.ts b/src/adapter/tstype.ts index cbb35f9389..22f38817af 100644 --- a/src/adapter/tstype.ts +++ b/src/adapter/tstype.ts @@ -1,4 +1,15 @@ -interface NetworkOptions { +export type Adapter = 'auto' | 'deconz' | 'ember' | 'zstack' | 'zboss' | 'zigate' | 'ezsp'; +export type ValidAdapter = 'deconz' | 'ember' | 'zstack' | 'zboss' | 'zigate' | 'ezsp'; +export type DiscoverableUSBAdapter = 'deconz' | 'ember' | 'zstack' | 'zboss' | 'zigate'; + +export type USBAdapterFingerprint = { + vendorId: string; + productId: string; + manufacturer?: string; + pathRegex: string; +}; + +export interface NetworkOptions { panID: number; extendedPanID?: number[]; channelList: number[]; @@ -6,14 +17,14 @@ interface NetworkOptions { networkKeyDistribute?: boolean; } -interface SerialPortOptions { +export interface SerialPortOptions { baudRate?: number; rtscts?: boolean; path?: string; - adapter?: 'zstack' | 'deconz' | 'zigate' | 'ezsp' | 'ember' | 'zboss' | 'auto'; + adapter?: Adapter; } -interface AdapterOptions { +export interface AdapterOptions { concurrent?: number; delay?: number; disableLED: boolean; @@ -21,16 +32,16 @@ interface AdapterOptions { forceStartWithInconsistentAdapterConfiguration?: boolean; } -interface CoordinatorVersion { +export interface CoordinatorVersion { type: string; meta: {[s: string]: number | string}; } -type DeviceType = 'Coordinator' | 'EndDevice' | 'Router' | 'Unknown'; +export type DeviceType = 'Coordinator' | 'EndDevice' | 'Router' | 'Unknown'; -type StartResult = 'resumed' | 'reset' | 'restored'; +export type StartResult = 'resumed' | 'reset' | 'restored'; -interface LQINeighbor { +export interface LQINeighbor { ieeeAddr: string; networkAddress: number; linkquality: number; @@ -38,21 +49,21 @@ interface LQINeighbor { depth: number; } -interface LQI { +export interface LQI { neighbors: LQINeighbor[]; } -interface RoutingTableEntry { +export interface RoutingTableEntry { destinationAddress: number; status: string; nextHop: number; } -interface RoutingTable { +export interface RoutingTable { table: RoutingTableEntry[]; } -interface Backup { +export interface Backup { adapterType: 'zStack'; time: string; meta: {[s: string]: number}; @@ -60,23 +71,8 @@ interface Backup { data: any; } -interface NetworkParameters { +export interface NetworkParameters { panID: number; extendedPanID: number; channel: number; } - -export { - SerialPortOptions, - NetworkOptions, - CoordinatorVersion, - DeviceType, - LQI, - LQINeighbor, - RoutingTable, - Backup, - NetworkParameters, - StartResult, - RoutingTableEntry, - AdapterOptions, -}; diff --git a/src/adapter/z-stack/adapter/zStackAdapter.ts b/src/adapter/z-stack/adapter/zStackAdapter.ts index bf93c6b4ff..6e1e207e9e 100644 --- a/src/adapter/z-stack/adapter/zStackAdapter.ts +++ b/src/adapter/z-stack/adapter/zStackAdapter.ts @@ -163,14 +163,6 @@ class ZStackAdapter extends Adapter { await this.znp.close(); } - public static async isValidPath(path: string): Promise { - return await Znp.isValidPath(path); - } - - public static async autoDetectPath(): Promise { - return await Znp.autoDetectPath(); - } - public async getCoordinatorIEEE(): Promise { return await this.queue.execute(async () => { this.checkInterpanLock(); diff --git a/src/adapter/z-stack/znp/znp.ts b/src/adapter/z-stack/znp/znp.ts index a43e9751c3..4169240db8 100755 --- a/src/adapter/z-stack/znp/znp.ts +++ b/src/adapter/z-stack/znp/znp.ts @@ -2,11 +2,10 @@ import assert from 'assert'; import events from 'events'; import net from 'net'; -import {Queue, RealpathSync, Wait, Waitress} from '../../../utils'; +import {Queue, Wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; import {ClusterId as ZdoClusterId} from '../../../zspec/zdo'; import {SerialPort} from '../../serialPort'; -import SerialPortUtils from '../../serialPortUtils'; import SocketPortUtils from '../../socketPortUtils'; import * as Constants from '../constants'; import {Frame as UnpiFrame, Parser as UnpiParser, Writer as UnpiWriter} from '../unpi'; @@ -38,13 +37,6 @@ interface WaitressMatcher { state?: number; } -const autoDetectDefinitions = [ - {manufacturer: 'Texas Instruments', vendorId: '0451', productId: '16c8'}, // CC2538 - {manufacturer: 'Texas Instruments', vendorId: '0451', productId: '16a8'}, // CC2531 - {manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'}, // CC1352P_2 and CC26X2R1 - {manufacturer: 'Electrolama', vendorId: '0403', productId: '6015'}, // ZZH -]; - class Znp extends events.EventEmitter { private path: string; private baudRate: number; @@ -198,31 +190,6 @@ class Znp extends events.EventEmitter { } } - public static async isValidPath(path: string): Promise { - // For TCP paths we cannot get device information, therefore we cannot validate it. - if (SocketPortUtils.isTcpPath(path)) { - return false; - } - - try { - return await SerialPortUtils.is(RealpathSync(path), autoDetectDefinitions); - } catch (error) { - logger.error(`Failed to determine if path is valid: '${error}'`, NS); - return false; - } - } - - public static async autoDetectPath(): Promise { - const paths = await SerialPortUtils.find(autoDetectDefinitions); - - // CC1352P_2 and CC26X2R1 lists as 2 USB devices with same manufacturer, productId and vendorId - // one is the actual chip interface, other is the XDS110. - // The chip is always exposed on the first one after alphabetical sorting. - paths.sort((a, b) => (a < b ? -1 : 1)); - - return paths.length > 0 ? paths[0] : undefined; - } - public async close(): Promise { logger.info('closing', NS); this.queue.clear(); diff --git a/src/adapter/zboss/adapter/zbossAdapter.ts b/src/adapter/zboss/adapter/zbossAdapter.ts index 087e13638d..307d50b031 100644 --- a/src/adapter/zboss/adapter/zbossAdapter.ts +++ b/src/adapter/zboss/adapter/zbossAdapter.ts @@ -4,26 +4,19 @@ import assert from 'assert'; import {Adapter, TsType} from '../..'; import {Backup} from '../../../models'; -import {Queue, RealpathSync, Waitress} from '../../../utils'; +import {Queue, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; import * as ZSpec from '../../../zspec'; import * as Zcl from '../../../zspec/zcl'; import * as Zdo from '../../../zspec/zdo'; import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; import {ZclPayload} from '../../events'; -import SerialPortUtils from '../../serialPortUtils'; -import SocketPortUtils from '../../socketPortUtils'; import {ZBOSSDriver} from '../driver'; import {CommandId, DeviceUpdateStatus} from '../enums'; import {FrameType, ZBOSSFrame} from '../frame'; const NS = 'zh:zboss'; -const autoDetectDefinitions = [ - // Nordic Zigbee NCP - {manufacturer: 'ZEPHYR', vendorId: '2fe3', productId: '0100'}, -]; - interface WaitressMatcher { address: number | string; endpoint: number; @@ -109,26 +102,6 @@ export class ZBOSSAdapter extends Adapter { } } - public static async isValidPath(path: string): Promise { - // For TCP paths we cannot get device information, therefore we cannot validate it. - if (SocketPortUtils.isTcpPath(path)) { - return false; - } - - try { - return await SerialPortUtils.is(RealpathSync(path), autoDetectDefinitions); - } catch (error) { - logger.debug(`Failed to determine if path is valid: '${error}'`, NS); - return false; - } - } - - public static async autoDetectPath(): Promise { - const paths = await SerialPortUtils.find(autoDetectDefinitions); - paths.sort((a, b) => (a < b ? -1 : 1)); - return paths.length > 0 ? paths[0] : null; - } - public async start(): Promise { logger.info(`ZBOSS Adapter starting`, NS); diff --git a/src/adapter/zigate/adapter/zigateAdapter.ts b/src/adapter/zigate/adapter/zigateAdapter.ts index 5c2670076c..4d0c364fca 100644 --- a/src/adapter/zigate/adapter/zigateAdapter.ts +++ b/src/adapter/zigate/adapter/zigateAdapter.ts @@ -530,14 +530,6 @@ class ZiGateAdapter extends Adapter { return {promise: waiter.start().promise, cancel}; } - public static async isValidPath(path: string): Promise { - return await Driver.isValidPath(path); - } - - public static async autoDetectPath(): Promise { - return await Driver.autoDetectPath(); - } - /** * InterPAN !!! not implemented */ diff --git a/src/adapter/zigate/driver/zigate.ts b/src/adapter/zigate/driver/zigate.ts index f2bb8a7190..cba3c849a9 100644 --- a/src/adapter/zigate/driver/zigate.ts +++ b/src/adapter/zigate/driver/zigate.ts @@ -13,7 +13,6 @@ import * as ZSpec from '../../../zspec'; import * as Zdo from '../../../zspec/zdo'; import {EndDeviceAnnounce, GenericZdoResponse, ResponseMap as ZdoResponseMap} from '../../../zspec/zdo/definition/tstypes'; import {SerialPort} from '../../serialPort'; -import SerialPortUtils from '../../serialPortUtils'; import SocketPortUtils from '../../socketPortUtils'; import {SerialPortOptions} from '../../tstype'; import {equal, ZiGateResponseMatcher, ZiGateResponseMatcherRule} from './commandType'; @@ -23,11 +22,6 @@ import ZiGateObject from './ziGateObject'; const NS = 'zh:zigate:driver'; -const autoDetectDefinitions = [ - {manufacturer: 'zigate_PL2303', vendorId: '067b', productId: '2303'}, - {manufacturer: 'zigate_cp2102', vendorId: '10c4', productId: 'ea60'}, -]; - const timeouts = { reset: 30000, default: 10000, @@ -204,15 +198,6 @@ export default class ZiGate extends EventEmitter { }); } - public static async isValidPath(path: string): Promise { - return await SerialPortUtils.is(path, autoDetectDefinitions); - } - - public static async autoDetectPath(): Promise { - const paths = await SerialPortUtils.find(autoDetectDefinitions); - return paths.length > 0 ? paths[0] : undefined; - } - public open(): Promise { return SocketPortUtils.isTcpPath(this.path) ? this.openSocketPort() : this.openSerialPort(); } diff --git a/src/utils/equalsPartial.ts b/src/utils/equalsPartial.ts deleted file mode 100644 index fd87d02942..0000000000 --- a/src/utils/equalsPartial.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Equals from 'fast-deep-equal/es6'; - -function equalsPartial(object: T, expected: Partial): boolean { - const entries = Object.entries(expected) as [keyof T, unknown][]; - return entries.every(([key, value]) => Equals(object[key], value)); -} - -export default equalsPartial; diff --git a/src/utils/index.ts b/src/utils/index.ts index d53ffe46bf..04973310bf 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,9 +1,8 @@ import * as BackupUtils from './backup'; -import EqualsPartial from './equalsPartial'; import Queue from './queue'; import RealpathSync from './realpathSync'; import * as Utils from './utils'; import Wait from './wait'; import Waitress from './waitress'; -export {Wait, Queue, Waitress, EqualsPartial, RealpathSync, BackupUtils, Utils}; +export {Wait, Queue, Waitress, RealpathSync, BackupUtils, Utils}; diff --git a/test/adapter/adapter.test.ts b/test/adapter/adapter.test.ts new file mode 100644 index 0000000000..91fef9db25 --- /dev/null +++ b/test/adapter/adapter.test.ts @@ -0,0 +1,456 @@ +import os from 'os'; + +import {Bonjour, BrowserConfig} from 'bonjour-service'; + +import {Adapter} from '../../src/adapter'; +import {DeconzAdapter} from '../../src/adapter/deconz/adapter'; +import {EmberAdapter} from '../../src/adapter/ember/adapter'; +import {EZSPAdapter} from '../../src/adapter/ezsp/adapter'; +import {SerialPort} from '../../src/adapter/serialPort'; +import {ZStackAdapter} from '../../src/adapter/z-stack/adapter'; +import {ZBOSSAdapter} from '../../src/adapter/zboss/adapter'; +import {ZiGateAdapter} from '../../src/adapter/zigate/adapter'; +import {DECONZ_CONBEE_II, EMBER_SKYCONNECT, ZBOSS_NORDIC, ZIGATE_PLUSV2, ZSTACK_CC2538} from '../mockAdapters'; + +const mockBonjourResult = jest.fn().mockImplementation((type) => ({ + name: 'Mock Adapter', + type: `${type}_mdns`, + port: '1122', + addresses: ['192.168.1.123'], + txt: { + radio_type: `${type}`, + baud_rate: 115200, + }, +})); +const mockBonjourFindOne = jest.fn().mockImplementation((opts: BrowserConfig | null, timeout: number, callback?: CallableFunction) => { + if (callback) { + callback(mockBonjourResult(opts?.type)); + } +}); +const mockBonjourDestroy = jest.fn(); + +jest.mock('bonjour-service', () => ({ + Bonjour: jest.fn().mockImplementation(() => ({ + findOne: mockBonjourFindOne, + destroy: mockBonjourDestroy, + })), +})); + +describe('Adapter', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + describe('mDNS discovery', () => { + beforeEach(() => { + mockBonjourResult.mockClear(); + mockBonjourFindOne.mockClear(); + mockBonjourDestroy.mockClear(); + }); + + it.each([ + ['deconz', DeconzAdapter], + ['ember', EmberAdapter], + ['ezsp', EZSPAdapter], + ['zstack', ZStackAdapter], + ['zboss', ZBOSSAdapter], + ['zigate', ZiGateAdapter], + ])('for %s', async (name, adapterCls) => { + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: `mdns://${name}`}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(adapterCls); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + baudRate: 115200, + path: 'tcp://192.168.1.123:1122', + adapter: name, + }); + }); + + it('for zstack as znp', async () => { + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: `mdns://znp`}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + baudRate: 115200, + path: 'tcp://192.168.1.123:1122', + adapter: 'zstack', + }); + }); + + it('times out', async () => { + mockBonjourResult.mockReturnValueOnce(null); + const fakeAdapterName = 'mdns_test_device'; + + expect(async () => { + await Adapter.create({panID: 0, channelList: []}, {path: `mdns://${fakeAdapterName}`}, 'test.db', {disableLED: false}); + }).rejects.toThrow(`Coordinator [${fakeAdapterName}] not found after timeout of 2000ms!`); + }); + + it('given invalid path', async () => { + expect(async () => { + await Adapter.create({panID: 0, channelList: []}, {path: `mdns://`}, 'test.db', {disableLED: false}); + }).rejects.toThrow(`No mdns device specified. You must specify the coordinator mdns service type after mdns://, e.g. mdns://my-adapter`); + }); + + it('returns invalid format', async () => { + mockBonjourResult.mockReturnValueOnce({ + name: 'Mock Adapter', + type: `my_adapter_mdns`, + port: '1122', + addresses: ['192.168.1.123'], + txt: { + radio_type: undefined, + baud_rate: 115200, + }, + }); + + expect(async () => { + await Adapter.create({panID: 0, channelList: []}, {path: `mdns://my_adapter`}, 'test.db', {disableLED: false}); + }).rejects.toThrow( + `Coordinator returned wrong Zeroconf format! The following values are expected:\n` + + `txt.radio_type, got: undefined\n` + + `txt.baud_rate, got: 115200\n` + + `address, got: 192.168.1.123\n` + + `port, got: 1122`, + ); + }); + + it('returns auto adapter', async () => { + mockBonjourResult.mockReturnValueOnce({ + name: 'Mock Adapter', + type: `my_adapter_mdns`, + port: '1122', + addresses: ['192.168.1.123'], + txt: { + radio_type: 'auto', + baud_rate: 115200, + }, + }); + + expect(async () => { + await Adapter.create({panID: 0, channelList: []}, {path: `mdns://my_adapter`}, 'test.db', {disableLED: false}); + }).rejects.toThrow(`Adapter auto is not supported.`); + }); + }); + + describe('TCP discovery', () => { + it('returns config', async () => { + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {path: `tcp://192.168.1.321:3456`, adapter: `zstack`}, + 'test.db.backup', + {disableLED: false}, + ); + + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: `tcp://192.168.1.321:3456`, + adapter: `zstack`, + }); + }); + + it('invalid path', async () => { + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: `tcp://192168.1.321:3456`, adapter: `zstack`}, 'test.db.backup', { + disableLED: false, + }); + }).rejects.toThrow(`Invalid TCP path, expected format: tcp://:`); + }); + + it('invalid adapter', async () => { + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: `tcp://192.168.1.321:3456`, adapter: `auto`}, 'test.db.backup', { + disableLED: false, + }); + }).rejects.toThrow(`Cannot discover TCP adapters at this time. Please specify valid 'adapter' and 'path' manually.`); + }); + }); + + describe('USB discovery', () => { + let listSpy: jest.SpyInstance; + let platformSpy: jest.SpyInstance; + + beforeAll(() => { + listSpy = jest.spyOn(SerialPort, 'list'); + listSpy.mockReturnValue([DECONZ_CONBEE_II, EMBER_SKYCONNECT, ZSTACK_CC2538, ZBOSS_NORDIC, ZIGATE_PLUSV2]); + + platformSpy = jest.spyOn(os, 'platform'); + platformSpy.mockReturnValue('linux'); + }); + + it('detects deconz from scratch', async () => { + listSpy.mockReturnValue([DECONZ_CONBEE_II]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(DeconzAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: DECONZ_CONBEE_II.path, + adapter: 'deconz', + }); + }); + + it('detects ember from scratch', async () => { + listSpy.mockReturnValue([EMBER_SKYCONNECT]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_SKYCONNECT.path, + adapter: 'ember', + }); + }); + + it('detects zstack from scratch', async () => { + listSpy.mockReturnValue([ZSTACK_CC2538]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); + }); + + it('detects zboss from scratch', async () => { + listSpy.mockReturnValue([ZBOSS_NORDIC]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZBOSS_NORDIC.path, + adapter: 'zboss', + }); + }); + + it('detects zigate from scratch', async () => { + listSpy.mockReturnValue([ZIGATE_PLUSV2]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZiGateAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZIGATE_PLUSV2.path, + adapter: 'zigate', + }); + }); + + it('detects ember from scratch on Windows', async () => { + platformSpy.mockReturnValueOnce('win32'); + listSpy.mockReturnValue([ + { + // Windows sample - Sonoff Dongle-E + path: 'COM3', + manufacturer: 'wch.cn', + serialNumber: '54DD002111', + pnpId: 'USB\\VID_1A86&PID_55D4\\54DD002111', + locationId: 'Port_#0005.Hub_#0001', + friendlyName: 'USB-Enhanced-SERIAL CH9102 (COM3)', + vendorId: '1A86', + productId: '55D4', + }, + ]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: 'COM3', + adapter: 'ember', + }); + }); + + it('detects deconz with specific config', async () => { + listSpy.mockReturnValue([DECONZ_CONBEE_II]); + + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'deconz', path: DECONZ_CONBEE_II.path}, + 'test.db.backup', + {disableLED: false}, + ); + + expect(adapter).toBeInstanceOf(DeconzAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: DECONZ_CONBEE_II.path, + adapter: 'deconz', + }); + }); + + it('detects ember with specific config', async () => { + listSpy.mockReturnValue([EMBER_SKYCONNECT]); + + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'ember', path: EMBER_SKYCONNECT.path}, + 'test.db.backup', + {disableLED: false}, + ); + + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_SKYCONNECT.path, + adapter: 'ember', + }); + }); + + it('detects ezsp with specific config', async () => { + listSpy.mockReturnValue([EMBER_SKYCONNECT]); + + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'ezsp', path: EMBER_SKYCONNECT.path}, + 'test.db.backup', + {disableLED: false}, + ); + + expect(adapter).toBeInstanceOf(EZSPAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_SKYCONNECT.path, + adapter: 'ezsp', + }); + }); + + it('detects zstack with specific config', async () => { + listSpy.mockReturnValue([ZSTACK_CC2538]); + + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'zstack', path: ZSTACK_CC2538.path}, + 'test.db.backup', + {disableLED: false}, + ); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); + }); + + it('detects zboss with specific config', async () => { + listSpy.mockReturnValue([ZBOSS_NORDIC]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss', path: ZBOSS_NORDIC.path}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZBOSS_NORDIC.path, + adapter: 'zboss', + }); + }); + + it('detects zigate with specific config', async () => { + listSpy.mockReturnValue([ZIGATE_PLUSV2]); + + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'zigate', path: ZIGATE_PLUSV2.path}, + 'test.db.backup', + {disableLED: false}, + ); + + expect(adapter).toBeInstanceOf(ZiGateAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZIGATE_PLUSV2.path, + adapter: 'zigate', + }); + }); + + it('detects with specific config with multiple adapters connected', async () => { + listSpy.mockReturnValue([DECONZ_CONBEE_II, ZSTACK_CC2538, EMBER_SKYCONNECT]); + + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'zstack', path: ZSTACK_CC2538.path}, + 'test.db.backup', + {disableLED: false}, + ); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); + }); + + it('fails to match specified adapter+path, tries to start anyway', async () => { + listSpy.mockReturnValue([]); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack', path: 'dev/ttyUSB0'}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: 'dev/ttyUSB0', + adapter: 'zstack', + }); + }); + + it('fails to match with different paths, tries to start anyway', async () => { + listSpy.mockReturnValue([DECONZ_CONBEE_II]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'deconz', path: '/dev/ttyUSB0'}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(DeconzAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyUSB0', + adapter: 'deconz', + }); + }); + + it('fails to match from scratch with different paths, throws', async () => { + listSpy.mockReturnValue([DECONZ_CONBEE_II]); + + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: '/dev/ttyUSB0'}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow(`Unable to find a valid USB adapter.`); + }); + + it('fails to match with incomplete port info, throws', async () => { + listSpy.mockReturnValue([{...DECONZ_CONBEE_II, vendorId: undefined}]); + + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow(`Unable to find a valid USB adapter.`); + }); + + it('fails to match specified adapter+path, throws invalid adapter', async () => { + listSpy.mockReturnValue([]); + + expect(async () => { + await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + // @ts-expect-error invalid on purpose + {adapter: 'invalid', path: 'dev/ttyUSB0'}, + 'test.db.backup', + {disableLED: false}, + ); + }).rejects.toThrow(`Adapter 'invalid' does not exists, possible options: zstack, deconz, zigate, ezsp, ember, zboss`); + }); + }); +}); diff --git a/test/adapter/z-stack/adapter.test.ts b/test/adapter/z-stack/adapter.test.ts index 1048334dc6..d5886a32d3 100644 --- a/test/adapter/z-stack/adapter.test.ts +++ b/test/adapter/z-stack/adapter.test.ts @@ -1384,9 +1384,6 @@ jest.mock('../../../src/utils/queue', () => { }); }); -Znp.isValidPath = jest.fn().mockReturnValue(true); -Znp.autoDetectPath = jest.fn().mockReturnValue('/dev/autodetected'); - const mocksClear = [mockLogger.debug, mockLogger.info, mockLogger.warning, mockLogger.error]; describe('zstack-adapter', () => { @@ -2078,17 +2075,6 @@ describe('zstack-adapter', () => { }); /* Original Tests */ - it('Is valid path', async () => { - const result = await ZStackAdapter.isValidPath('/dev/autodetected'); - expect(result).toBeTruthy(); - expect(Znp.isValidPath).toHaveBeenCalledWith('/dev/autodetected'); - }); - - it('Auto detect path', async () => { - const result = await ZStackAdapter.autoDetectPath(); - expect(result).toBe('/dev/autodetected'); - expect(Znp.autoDetectPath).toHaveBeenCalledTimes(1); - }); it('Call znp constructor', async () => { expect(Znp).toHaveBeenCalledWith('dummy', 800, false); diff --git a/test/adapter/z-stack/znp.test.ts b/test/adapter/z-stack/znp.test.ts index d88804220f..b8045912ef 100644 --- a/test/adapter/z-stack/znp.test.ts +++ b/test/adapter/z-stack/znp.test.ts @@ -202,25 +202,6 @@ describe('ZNP', () => { expect(mockSerialPortOnce).toHaveBeenCalledTimes(2); }); - it('Open autodetect port', async () => { - mockSerialPortList.mockReturnValue([ - {manufacturer: 'Not texas instruments', vendorId: '0451', productId: '16a8', path: '/dev/autodetected2'}, - {path: '/dev/tty.usbmodemL43001T22', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'}, - {path: '/dev/tty.usbmodemL43001T24', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'}, - {path: '/dev/tty.usbmodemL43001T21', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'}, - ]); - - expect(await Znp.autoDetectPath()).toBe('/dev/tty.usbmodemL43001T21'); - }); - - it('Autodetect port error when there are not available devices', async () => { - mockSerialPortList.mockReturnValue([ - {manufacturer: 'Not texas instruments', vendorId: '0451', productId: '16a8', path: '/dev/autodetected2'}, - ]); - - expect(await Znp.autoDetectPath()).toBeUndefined(); - }); - it('Open and close tcp port', async () => { znp = new Znp('tcp://localhost:8080', 100, false); await znp.open(); @@ -251,43 +232,6 @@ describe('ZNP', () => { expect(znp.isInitialized()).toBeFalsy(); }); - it('Check if tcp path is valid', async () => { - expect(await Znp.isValidPath('tcp://192.168.2.1:8080')).toBeFalsy(); - expect(await Znp.isValidPath('tcp://localhost:8080')).toBeFalsy(); - expect(await Znp.isValidPath('tcp://192.168.2.1')).toBeFalsy(); - expect(await Znp.isValidPath('tcp://localhost')).toBeFalsy(); - expect(await Znp.isValidPath('tcp')).toBeFalsy(); - }); - - it('Check if path is valid', async () => { - mockSerialPortList.mockReturnValue([ - {manufacturer: 'Not texas instruments', vendorId: '0451', productId: '16a8', path: '/dev/autodetected2'}, - {path: '/dev/tty.usbmodemL43001T22', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'}, - {path: '/dev/tty.usbmodemL43001T24', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'}, - {path: '/dev/tty.usbmodemL43001T21', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'}, - ]); - - expect(await Znp.isValidPath('/dev/tty.usbmodemL43001T21')).toBeTruthy(); - expect(await Znp.isValidPath('/dev/autodetected2')).toBeFalsy(); - }); - - it('Check if path is valid; return false when path does not exist in device list', async () => { - mockSerialPortList.mockReturnValue([ - {manufacturer: 'Not texas instruments', vendorId: '0451', productId: '16a8', path: '/dev/autodetected2'}, - {path: '/dev/tty.usbmodemL43001T22', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'}, - {path: '/dev/tty.usbmodemL43001T24', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'}, - {path: '/dev/tty.usbmodemL43001T21', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'}, - ]); - - expect(await Znp.isValidPath('/dev/notexisting')).toBeFalsy(); - }); - - it('Check if path is valid path resolve fails', async () => { - mockRealPathSyncError = true; - expect(await Znp.isValidPath('/dev/tty.usbmodemL43001T21')).toBeFalsy(); - mockRealPathSyncError = false; - }); - it('Open with error', async () => { mockSerialPortAsyncOpen.mockImplementationOnce(() => { return new Promise((resolve, reject) => { diff --git a/test/controller.test.ts b/test/controller.test.ts index 8449126b7e..317f9aeee1 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -439,33 +439,7 @@ const getTempFile = (filename: string): string => { return path.join(TEMP_PATH, filename); }; -// Mock static methods -const mockZStackAdapterIsValidPath = jest.fn().mockReturnValue(true); -const mockZStackAdapterAutoDetectPath = jest.fn().mockReturnValue('/dev/autodetected'); -ZStackAdapter.isValidPath = mockZStackAdapterIsValidPath; -ZStackAdapter.autoDetectPath = mockZStackAdapterAutoDetectPath; - -const mockDeconzAdapterIsValidPath = jest.fn().mockReturnValue(true); -const mockDeconzAdapterAutoDetectPath = jest.fn().mockReturnValue('/dev/autodetected'); -DeconzAdapter.isValidPath = mockDeconzAdapterIsValidPath; -DeconzAdapter.autoDetectPath = mockDeconzAdapterAutoDetectPath; - -const mockZiGateAdapterIsValidPath = jest.fn().mockReturnValue(true); -const mockZiGateAdapterAutoDetectPath = jest.fn().mockReturnValue('/dev/autodetected'); -ZiGateAdapter.isValidPath = mockZiGateAdapterIsValidPath; -ZiGateAdapter.autoDetectPath = mockZiGateAdapterAutoDetectPath; - -const mocksRestore = [ - mockAdapterPermitJoin, - mockAdapterStop, - mocksendZclFrameToAll, - mockZStackAdapterIsValidPath, - mockZStackAdapterAutoDetectPath, - mockDeconzAdapterIsValidPath, - mockDeconzAdapterAutoDetectPath, - mockZiGateAdapterIsValidPath, - mockZiGateAdapterAutoDetectPath, -]; +const mocksRestore = [mockAdapterPermitJoin, mockAdapterStop, mocksendZclFrameToAll]; const events: { deviceJoined: Events.DeviceJoinedPayload[]; @@ -499,8 +473,8 @@ const options = { serialPort: { baudRate: 115200, rtscts: true, - path: '/dummy/conbee', - adapter: undefined, + path: '/dev/ttyUSB0', + adapter: 'zstack', }, adapter: { disableLED: false, @@ -570,7 +544,7 @@ describe('Controller', () => { extendedPanID: [221, 221, 221, 221, 221, 221, 221, 221], channelList: [15], }, - {baudRate: 115200, path: '/dummy/conbee', rtscts: true, adapter: undefined}, + {baudRate: 115200, path: '/dev/ttyUSB0', rtscts: true, adapter: 'zstack'}, backupPath, {disableLED: false}, ); @@ -7257,414 +7231,6 @@ describe('Controller', () => { expect(endpoint.getClusterAttributeValue('msOccupancySensing', 'occupancy')).toBe(0); }); - it('Adapter create', async () => { - mockZStackAdapterIsValidPath.mockReturnValueOnce(true); - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: '/dev/bla', baudRate: 100, rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - expect(mockZStackAdapterIsValidPath).toHaveBeenCalledWith('/dev/bla'); - expect(ZStackAdapter).toHaveBeenCalledWith( - { - panID: 0, - channelList: [], - }, - {baudRate: 100, path: '/dev/bla', rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - }); - - it('Adapter create continue when is valid path fails', async () => { - mockZStackAdapterIsValidPath.mockImplementationOnce(() => { - throw new Error('failed'); - }); - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: '/dev/bla', baudRate: 100, rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - expect(mockZStackAdapterIsValidPath).toHaveBeenCalledWith('/dev/bla'); - expect(ZStackAdapter).toHaveBeenCalledWith( - { - panID: 0, - channelList: [], - }, - {baudRate: 100, path: '/dev/bla', rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - }); - - it('Adapter create auto detect', async () => { - mockZStackAdapterIsValidPath.mockReturnValueOnce(true); - mockZStackAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: undefined, baudRate: 100, rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - expect(ZStackAdapter).toHaveBeenCalledWith( - { - panID: 0, - channelList: [], - }, - {baudRate: 100, path: '/dev/test', rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - }); - - it('Adapter mdns timeout test', async () => { - const fakeAdapterName = 'mdns_test_device'; - - try { - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - } catch (e) { - expect(e).toStrictEqual(new Error(`Coordinator [${fakeAdapterName}] not found after timeout of 2000ms!`)); - } - }); - - it('Adapter mdns without type test', async () => { - const fakeAdapterName = ''; - - try { - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - } catch (e) { - expect(e).toStrictEqual( - new Error(`No mdns device specified. You must specify the coordinator mdns service type after mdns://, e.g. mdns://my-adapter`), - ); - } - }); - - it('Adapter mdns wrong Zeroconf test', async () => { - const fakeAdapterName = 'mdns_test_device'; - const fakeIp = '111.111.111.111'; - const fakePort = 6638; - const fakeBaud = '115200'; - - // @ts-expect-error mock - Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction): Browser { - setTimeout(() => { - callback?.({name: 'fakeAdapter', type: fakeAdapterName, port: fakePort, addresses: [fakeIp], txt: {baud_rate: fakeBaud}}); - }, 200); - }; - - try { - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - } catch (e) { - expect(e).toStrictEqual( - new Error( - `Coordinator returned wrong Zeroconf format! The following values are expected:\n` + - `txt.radio_type, got: undefined\n` + - `txt.baud_rate, got: 115200\n` + - `address, got: 111.111.111.111\n` + - `port, got: 6638`, - ), - ); - } - }); - - it('Adapter mdns detection ezsp test', async () => { - const fakeAdapterName = 'mdns_test_device'; - const fakeIp = '111.111.111.111'; - const fakePort = 6638; - const fakeRadio = 'ezsp'; - const fakeBaud = '115200'; - - // @ts-expect-error mock - Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction): Browser { - setTimeout(() => { - callback?.({ - name: 'fakeAdapter', - type: fakeAdapterName, - port: fakePort, - addresses: [fakeIp], - txt: {radio_type: fakeRadio, baud_rate: fakeBaud}, - }); - }, 200); - }; - - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - - expect(mockLogger.info.mock.calls[0][0]).toBe(`Starting mdns discovery for coordinator: ${fakeAdapterName}`); - expect(mockLogger.info.mock.calls[1][0]).toBe(`Coordinator Ip: ${fakeIp}`); - expect(mockLogger.info.mock.calls[2][0]).toBe(`Coordinator Port: ${fakePort}`); - expect(mockLogger.info.mock.calls[3][0]).toBe(`Coordinator Radio: ${fakeRadio}`); - expect(mockLogger.info.mock.calls[4][0]).toBe(`Coordinator Baud: ${fakeBaud}\n`); - }); - - it('Adapter mdns detection unsupported adapter test', async () => { - const fakeAdapterName = 'mdns_test_device'; - const fakeIp = '111.111.111.111'; - const fakePort = 6638; - const fakeRadio = 'auto'; - const fakeBaud = '115200'; - - // @ts-expect-error mock - Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction): Browser { - setTimeout(() => { - callback?.({ - name: 'fakeAdapter', - type: fakeAdapterName, - port: fakePort, - addresses: [fakeIp], - txt: {radio_type: fakeRadio, baud_rate: fakeBaud}, - }); - }, 200); - }; - - try { - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - } catch (e) { - expect(e).toStrictEqual(new Error(`Adapter ${fakeRadio} is not supported.`)); - } - }); - - it('Adapter mdns detection zstack test', async () => { - const fakeAdapterName = 'mdns_test_device'; - const fakeIp = '111.111.111.111'; - const fakePort = 6638; - const fakeRadio = 'znp'; - const fakeBaud = '115200'; - - // @ts-expect-error mock - Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction): Browser { - setTimeout(() => { - callback?.({ - name: 'fakeAdapter', - type: fakeAdapterName, - port: fakePort, - addresses: [fakeIp], - txt: {radio_type: fakeRadio, baud_rate: fakeBaud}, - }); - }, 200); - }; - - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - - expect(mockLogger.info.mock.calls[0][0]).toBe(`Starting mdns discovery for coordinator: ${fakeAdapterName}`); - expect(mockLogger.info.mock.calls[1][0]).toBe(`Coordinator Ip: ${fakeIp}`); - expect(mockLogger.info.mock.calls[2][0]).toBe(`Coordinator Port: ${fakePort}`); - expect(mockLogger.info.mock.calls[3][0]).toBe(`Coordinator Radio: zstack`); - expect(mockLogger.info.mock.calls[4][0]).toBe(`Coordinator Baud: ${fakeBaud}\n`); - }); - - it('Adapter create auto detect nothing found', async () => { - mockZStackAdapterIsValidPath.mockReturnValueOnce(false); - mockZStackAdapterAutoDetectPath.mockReturnValueOnce(null); - - try { - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: undefined, baudRate: 100, rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - } catch (e) { - expect(e).toStrictEqual(new Error('No path provided and failed to auto detect path')); - } - }); - - it('Adapter create with unknown path should take ZStackAdapter by default', async () => { - mockZStackAdapterIsValidPath.mockReturnValueOnce(false); - mockZStackAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: undefined, baudRate: 100, rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - expect(ZStackAdapter).toHaveBeenCalledWith( - { - panID: 0, - channelList: [], - }, - {baudRate: 100, path: '/dev/test', rtscts: false, adapter: undefined}, - 'test.db', - { - disableLED: false, - }, - ); - }); - - it('Adapter create should be able to specify adapter', async () => { - mockZStackAdapterIsValidPath.mockReturnValueOnce(false); - mockZStackAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); - mockDeconzAdapterIsValidPath.mockReturnValueOnce(false); - mockDeconzAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); - mockZiGateAdapterIsValidPath.mockReturnValueOnce(false); - mockZiGateAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: undefined, baudRate: 100, rtscts: false, adapter: 'deconz'}, - 'test.db', - { - disableLED: false, - }, - ); - expect(DeconzAdapter).toHaveBeenCalledWith( - { - panID: 0, - channelList: [], - }, - {baudRate: 100, path: '/dev/test', rtscts: false, adapter: 'deconz'}, - 'test.db', - { - disableLED: false, - }, - ); - await Adapter.create( - { - panID: 0, - channelList: [], - }, - {path: undefined, baudRate: 100, rtscts: false, adapter: 'zigate'}, - 'test.db', - { - disableLED: false, - }, - ); - expect(ZiGateAdapter).toHaveBeenCalledWith( - { - panID: 0, - channelList: [], - }, - {baudRate: 100, path: '/dev/test', rtscts: false, adapter: 'zigate'}, - 'test.db', - { - disableLED: false, - }, - ); - }); - - it('Adapter create should throw on uknown adapter', async () => { - mockZStackAdapterIsValidPath.mockReturnValueOnce(false); - mockZStackAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); - mockDeconzAdapterIsValidPath.mockReturnValueOnce(false); - mockDeconzAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); - - try { - await Adapter.create( - { - panID: 0, - channelList: [], - }, - { - path: undefined, - baudRate: 100, - rtscts: false, - // @ts-expect-error bad on purpose - adapter: 'efr', - }, - 'test.db', - { - disableLED: false, - }, - ); - } catch (e) { - expect(e).toStrictEqual(new Error(`Adapter 'efr' does not exists, possible options: zstack, deconz, zigate, ezsp, ember, zboss`)); - } - }); - it('Emit read from device', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); @@ -8438,6 +8004,7 @@ describe('Controller', () => { it('Should handle comissioning frame gracefully', async () => { await controller.start(); + mockLogger.error.mockClear(); const buffer = Buffer.from([25, 10, 2, 11, 254, 0]); const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.greenPower.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); await mockAdapterEvents['zclPayload']({ diff --git a/test/mockAdapters.ts b/test/mockAdapters.ts new file mode 100644 index 0000000000..a5353ab602 --- /dev/null +++ b/test/mockAdapters.ts @@ -0,0 +1,29 @@ +export const DECONZ_CONBEE_II = { + path: '/dev/serial/by-id/usb-dresden_elektronik_ingenieurtechnik_GmbH_ConBee_II_DE2132111-if00', + vendorId: '1cf1', + productId: '0030', + manufacturer: 'dresden elektronik ingenieurtechnik GmbH', +}; +export const EMBER_SKYCONNECT = { + path: '/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_3abe54797c91ed118fc3cad13b20a111-if00-port0', + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'Nabu Casa', +}; +export const ZSTACK_CC2538 = { + path: '/dev/serial/by-id/usb-Texas_Instruments_CC2538_USB_CDC-if00', + vendorId: '0451', + productId: '16c8', + manufacturer: 'Texas Instruments', +}; +export const ZBOSS_NORDIC = { + path: '/dev/serial/by-id/mocked-zephyr-nordic', + vendorId: '2fe3', + productId: '0100', + manufacturer: 'ZEPHYR', +}; +export const ZIGATE_PLUSV2 = { + path: '/dev/serial/by-id/usb-FTDI_ZiGate_ZIGATE+-if00-port0', + vendorId: '0403', + productId: '6015', +}; From f0f461551878e62ce9d1b3e47a32eb9040f43d2c Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:44:15 +0200 Subject: [PATCH 04/13] Add alt regex matching on `pnpId`. --- src/adapter/adapterDiscovery.ts | 34 +++++++++++++++++++++++++-------- test/adapter/adapter.test.ts | 30 ++++++++++++++++++++++++++++- test/mockAdapters.ts | 6 +++--- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index 3db8619583..bd99957331 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -187,7 +187,8 @@ const USB_FINGERPRINTS: Record vendorId: '2fe3', productId: '0100', manufacturer: 'ZEPHYR', - pathRegex: '.*ZEPHYR.*', // TODO + // /dev/serial/by-id/usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00 + pathRegex: '.*ZEPHYR.*', }, ], zigate: [ @@ -230,6 +231,26 @@ async function getSerialPortList(): Promise { return portInfos; } +/** + * Case insensitive string matching. + * @param str1 + * @param str2 + * @returns + */ +function matchString(str1: string, str2: string): boolean { + return str1.localeCompare(str2, undefined, {sensitivity: 'base'}) === 0; +} + +/** + * Case insensitive regex matching. + * @param regexStr Passed to RegExp constructor. + * @param str Always returns false if undefined. + * @returns + */ +function matchRegex(regexStr: string, str?: string): boolean { + return str !== undefined && new RegExp(regexStr, 'i').test(str); +} + function matchUSBFingerprint( portInfo: PortInfo, isWindows: boolean, @@ -242,13 +263,10 @@ function matchUSBFingerprint( for (const entry of entries) { if ( - portInfo.vendorId.localeCompare(entry.vendorId, undefined, {sensitivity: 'base'}) === 0 && - portInfo.productId.localeCompare(entry.productId, undefined, {sensitivity: 'base'}) === 0 && - (!entry.manufacturer || - !portInfo.manufacturer || - portInfo.manufacturer.localeCompare(entry.manufacturer, undefined, {sensitivity: 'base'}) === 0 || - isWindows) && - (!entry.pathRegex || new RegExp(entry.pathRegex, 'i').test(portInfo.path) || isWindows) + matchString(portInfo.vendorId, entry.vendorId) && + matchString(portInfo.productId, entry.productId) && + (!entry.manufacturer || !portInfo.manufacturer || matchString(portInfo.manufacturer, entry.manufacturer) || isWindows) && + (!entry.pathRegex || matchRegex(entry.pathRegex, portInfo.path) || matchRegex(entry.pathRegex, portInfo.pnpId) || isWindows) ) { return [portInfo.path, entry]; } diff --git a/test/adapter/adapter.test.ts b/test/adapter/adapter.test.ts index 91fef9db25..ba5b6dcfb0 100644 --- a/test/adapter/adapter.test.ts +++ b/test/adapter/adapter.test.ts @@ -245,7 +245,7 @@ describe('Adapter', () => { }); }); - it('detects ember from scratch on Windows', async () => { + it('detects from scratch on Windows', async () => { platformSpy.mockReturnValueOnce('win32'); listSpy.mockReturnValue([ { @@ -271,6 +271,19 @@ describe('Adapter', () => { }); }); + it('detects from scratch with pnpId', async () => { + listSpy.mockReturnValue([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyUSB0', + adapter: 'zboss', + }); + }); + it('detects deconz with specific config', async () => { listSpy.mockReturnValue([DECONZ_CONBEE_II]); @@ -394,6 +407,21 @@ describe('Adapter', () => { }); }); + it('detects with specific config with pnpId', async () => { + listSpy.mockReturnValue([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss', path: '/dev/ttyUSB0'}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyUSB0', + adapter: 'zboss', + }); + }); + it('fails to match specified adapter+path, tries to start anyway', async () => { listSpy.mockReturnValue([]); const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack', path: 'dev/ttyUSB0'}, 'test.db.backup', { diff --git a/test/mockAdapters.ts b/test/mockAdapters.ts index a5353ab602..123de3dbb3 100644 --- a/test/mockAdapters.ts +++ b/test/mockAdapters.ts @@ -6,18 +6,18 @@ export const DECONZ_CONBEE_II = { }; export const EMBER_SKYCONNECT = { path: '/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_3abe54797c91ed118fc3cad13b20a111-if00-port0', - vendorId: '10c4', + vendorId: '10C4', // uppercased for extra coverage productId: 'ea60', manufacturer: 'Nabu Casa', }; export const ZSTACK_CC2538 = { path: '/dev/serial/by-id/usb-Texas_Instruments_CC2538_USB_CDC-if00', vendorId: '0451', - productId: '16c8', + productId: '16C8', // uppercased for extra coverage manufacturer: 'Texas Instruments', }; export const ZBOSS_NORDIC = { - path: '/dev/serial/by-id/mocked-zephyr-nordic', + path: '/dev/serial/by-id/usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00', vendorId: '2fe3', productId: '0100', manufacturer: 'ZEPHYR', From 00e0774396f379e1566459e9fe4f6960c855decd Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:30:37 +0200 Subject: [PATCH 05/13] Feedback. --- src/adapter/adapterDiscovery.ts | 35 ++++++++++++++++++----------- test/adapter/adapter.test.ts | 40 ++++++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index bd99957331..575828ad2e 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -86,6 +86,7 @@ const USB_FINGERPRINTS: Record productId: '55d4', manufacturer: 'ITEAD', // /dev/serial/by-id/usb-ITEAD_SONOFF_Zigbee_3.0_USB_Dongle_Plus_V2_20240122184111-if00 + // /dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_186ff44314e2ed11b891eb5162c61111-if00-port0 pathRegex: '.*sonoff.*plus.*', }, // { @@ -156,7 +157,7 @@ const USB_FINGERPRINTS: Record vendorId: '', productId: '', // manufacturer: '', - // /dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-07p7_be9faa0786e1ea11bd68dc2d9a583cc7-if00-port0 + // /dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-07p7_be9faa0786e1ea11bd68dc2d9a583111-if00-port0 pathRegex: '.*SLZB-07p7.*', }, { @@ -187,7 +188,7 @@ const USB_FINGERPRINTS: Record vendorId: '2fe3', productId: '0100', manufacturer: 'ZEPHYR', - // /dev/serial/by-id/usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00 + // /dev/serial/by-id/usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DAD111-if00 pathRegex: '.*ZEPHYR.*', }, ], @@ -377,7 +378,7 @@ export async function findTCPAdapter(path: string, adapter?: Adapter): Promise<[ } if (!adapter || adapter === 'auto') { - throw new Error(`Cannot discover TCP adapters at this time. Please specify valid 'adapter' and 'path' manually.`); + throw new Error(`Cannot discover TCP adapters at this time. Specify valid 'adapter' and 'port' in your configuration.`); } return [adapter, path]; @@ -408,23 +409,31 @@ export async function discoverAdapter( } else if (path.startsWith('tcp://')) { return await findTCPAdapter(path, adapter); } else if (adapter && adapter !== 'auto') { - const matched = await matchUSBAdapter(adapter, path); + try { + const matched = await matchUSBAdapter(adapter, path); - /* istanbul ignore else */ - if (!matched) { - logger.error(`Unable to match USB adapter: ${adapter} | ${path}`, NS); + /* istanbul ignore else */ + if (!matched) { + logger.error(`Unable to match USB adapter: ${adapter} | ${path}`, NS); + } + } catch (error) { + logger.error(`Error while trying to match USB adapter (${(error as Error).message}).`, NS); } return [adapter, path]; } } - // default to matching USB - const match = await findUSBAdapter(path); + try { + // default to matching USB + const match = await findUSBAdapter(path); - if (!match) { - throw new Error(`Unable to find a valid USB adapter.`); - } + if (!match) { + throw new Error(`No valid USB adapter found`); + } - return match; + return match; + } catch (error) { + throw new Error(`USB adapter discovery error (${(error as Error).message}). Specify valid 'adapter' and 'port' in your configuration.`); + } } diff --git a/test/adapter/adapter.test.ts b/test/adapter/adapter.test.ts index ba5b6dcfb0..e7bfca87d1 100644 --- a/test/adapter/adapter.test.ts +++ b/test/adapter/adapter.test.ts @@ -164,7 +164,7 @@ describe('Adapter', () => { await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: `tcp://192.168.1.321:3456`, adapter: `auto`}, 'test.db.backup', { disableLED: false, }); - }).rejects.toThrow(`Cannot discover TCP adapters at this time. Please specify valid 'adapter' and 'path' manually.`); + }).rejects.toThrow(`Cannot discover TCP adapters at this time. Specify valid 'adapter' and 'port' in your configuration.`); }); }); @@ -456,7 +456,7 @@ describe('Adapter', () => { expect(async () => { await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: '/dev/ttyUSB0'}, 'test.db.backup', {disableLED: false}); - }).rejects.toThrow(`Unable to find a valid USB adapter.`); + }).rejects.toThrow(`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`); }); it('fails to match with incomplete port info, throws', async () => { @@ -464,7 +464,7 @@ describe('Adapter', () => { expect(async () => { await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - }).rejects.toThrow(`Unable to find a valid USB adapter.`); + }).rejects.toThrow(`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`); }); it('fails to match specified adapter+path, throws invalid adapter', async () => { @@ -480,5 +480,39 @@ describe('Adapter', () => { ); }).rejects.toThrow(`Adapter 'invalid' does not exists, possible options: zstack, deconz, zigate, ezsp, ember, zboss`); }); + + it('detecting from scratch fails to get SerialPort.list', async () => { + listSpy.mockRejectedValueOnce(new Error('spawn udevadm ENOENT')); + + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow(`USB adapter discovery error (spawn udevadm ENOENT). Specify valid 'adapter' and 'port' in your configuration.`); + }); + + it('detecting with auto config fails to get SerialPort.list', async () => { + listSpy.mockRejectedValueOnce(new Error('spawn udevadm ENOENT')); + + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'auto'}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow(`USB adapter discovery error (spawn udevadm ENOENT). Specify valid 'adapter' and 'port' in your configuration.`); + }); + + it('detecting with specific config fails to get SerialPort.list, uses config anyway', async () => { + listSpy.mockRejectedValueOnce(new Error('spawn udevadm ENOENT')); + + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'zstack', path: ZSTACK_CC2538.path}, + 'test.db.backup', + {disableLED: false}, + ); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); + }); }); }); From f3f841553e8c482eacbbfb25c4371f37771e8088 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:50:09 +0200 Subject: [PATCH 06/13] Lower log level on specified config matching fail. --- src/adapter/adapterDiscovery.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index 575828ad2e..b2b2e39f37 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -414,10 +414,10 @@ export async function discoverAdapter( /* istanbul ignore else */ if (!matched) { - logger.error(`Unable to match USB adapter: ${adapter} | ${path}`, NS); + logger.debug(`Unable to match USB adapter: ${adapter} | ${path}`, NS); } } catch (error) { - logger.error(`Error while trying to match USB adapter (${(error as Error).message}).`, NS); + logger.debug(`Error while trying to match USB adapter (${(error as Error).message}).`, NS); } return [adapter, path]; From 1ccfbadbf678f7b06c3d702c53122df69b79e86a Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:58:48 +0200 Subject: [PATCH 07/13] Improve matching logic to avoid false positive. --- src/adapter/adapterDiscovery.ts | 47 ++++++++---- test/adapter/adapter.test.ts | 131 +++++++++++++++++++++++++------- test/mockAdapters.ts | 16 +++- 3 files changed, 152 insertions(+), 42 deletions(-) diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index b2b2e39f37..e4cba05e7e 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -117,7 +117,7 @@ const USB_FINGERPRINTS: Record // Sonoff ZBDongle-P (CC2652P) vendorId: '10c4', productId: 'ea60', - // manufacturer: '', + manufacturer: 'ITEAD', // /dev/serial/by-id/usb-Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus_0111-if00-port0 // /dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_b8b49abd27a6ed11a280eba32981d111-if00-port0 pathRegex: '.*sonoff.*plus.*', @@ -152,14 +152,14 @@ const USB_FINGERPRINTS: Record manufacturer: 'Texas Instruments', pathRegex: '.*CC26X2R1.*', // TODO }, - { - // SMLight slzb-07p7 - vendorId: '', - productId: '', - // manufacturer: '', - // /dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-07p7_be9faa0786e1ea11bd68dc2d9a583111-if00-port0 - pathRegex: '.*SLZB-07p7.*', - }, + // { + // // TODO: SMLight slzb-07p7 + // vendorId: '', + // productId: '', + // // manufacturer: '', + // // /dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-07p7_be9faa0786e1ea11bd68dc2d9a583111-if00-port0 + // pathRegex: '.*SLZB-07p7.*', + // }, { // TubesZB ? vendorId: '10c4', @@ -218,6 +218,11 @@ const USB_FINGERPRINTS: Record ], }; +/** + * Vendor and Product IDs that are prone to conflict if only matching on vendorId+productId. + */ +const USB_FINGERPRINTS_CONFLICT_IDS: ReadonlyArray = ['10c4:ea60']; + async function getSerialPortList(): Promise { const portInfos = await SerialPort.list(); @@ -256,6 +261,7 @@ function matchUSBFingerprint( portInfo: PortInfo, isWindows: boolean, entries: USBAdapterFingerprint[], + conflictProne: boolean, ): [PortInfo['path'], USBAdapterFingerprint] | undefined { if (!portInfo.vendorId || !portInfo.productId) { // port info is missing essential information for proper matching, ignore it @@ -263,9 +269,19 @@ function matchUSBFingerprint( } for (const entry of entries) { - if ( - matchString(portInfo.vendorId, entry.vendorId) && - matchString(portInfo.productId, entry.productId) && + if (!matchString(portInfo.vendorId, entry.vendorId) || !matchString(portInfo.productId, entry.productId)) { + continue; + } + + if (conflictProne) { + // if vendor+product combo is conflict prone, enforce at least one of manufacturer or pathRegex to match to avoid false positive + if ( + (entry.manufacturer && portInfo.manufacturer && matchString(portInfo.manufacturer, entry.manufacturer)) || + (entry.pathRegex && (matchRegex(entry.pathRegex, portInfo.path) || matchRegex(entry.pathRegex, portInfo.pnpId))) + ) { + return [portInfo.path, entry]; + } + } else if ( (!entry.manufacturer || !portInfo.manufacturer || matchString(portInfo.manufacturer, entry.manufacturer) || isWindows) && (!entry.pathRegex || matchRegex(entry.pathRegex, portInfo.path) || matchRegex(entry.pathRegex, portInfo.pnpId) || isWindows) ) { @@ -283,7 +299,8 @@ export async function matchUSBAdapter(adapter: ValidAdapter, path: string): Prom continue; } - const match = matchUSBFingerprint(portInfo, isWindows, USB_FINGERPRINTS[adapter === 'ezsp' ? 'ember' : adapter]); + const conflictProne = USB_FINGERPRINTS_CONFLICT_IDS.includes(`${portInfo.vendorId}:${portInfo.productId}`); + const match = matchUSBFingerprint(portInfo, isWindows, USB_FINGERPRINTS[adapter === 'ezsp' ? 'ember' : adapter], conflictProne); /* istanbul ignore else */ if (match) { @@ -303,8 +320,10 @@ export async function findUSBAdapter(path?: string): Promise<[adapter: Discovera continue; } + const conflictProne = USB_FINGERPRINTS_CONFLICT_IDS.includes(`${portInfo.vendorId}:${portInfo.productId}`); + for (const key in USB_FINGERPRINTS) { - const match = matchUSBFingerprint(portInfo, isWindows, USB_FINGERPRINTS[key as DiscoverableUSBAdapter]!); + const match = matchUSBFingerprint(portInfo, isWindows, USB_FINGERPRINTS[key as DiscoverableUSBAdapter]!, conflictProne); if (match) { logger.info(`Matched adapter: ${JSON.stringify(portInfo)} => ${key}: ${JSON.stringify(match[1])}`, NS); diff --git a/test/adapter/adapter.test.ts b/test/adapter/adapter.test.ts index e7bfca87d1..5f28ac1ef1 100644 --- a/test/adapter/adapter.test.ts +++ b/test/adapter/adapter.test.ts @@ -10,7 +10,7 @@ import {SerialPort} from '../../src/adapter/serialPort'; import {ZStackAdapter} from '../../src/adapter/z-stack/adapter'; import {ZBOSSAdapter} from '../../src/adapter/zboss/adapter'; import {ZiGateAdapter} from '../../src/adapter/zigate/adapter'; -import {DECONZ_CONBEE_II, EMBER_SKYCONNECT, ZBOSS_NORDIC, ZIGATE_PLUSV2, ZSTACK_CC2538} from '../mockAdapters'; +import {DECONZ_CONBEE_II, EMBER_SKYCONNECT, EMBER_ZBDONGLE_E, ZBOSS_NORDIC, ZIGATE_PLUSV2, ZSTACK_CC2538, ZSTACK_ZBDONGLE_P} from '../mockAdapters'; const mockBonjourResult = jest.fn().mockImplementation((type) => ({ name: 'Mock Adapter', @@ -174,14 +174,14 @@ describe('Adapter', () => { beforeAll(() => { listSpy = jest.spyOn(SerialPort, 'list'); - listSpy.mockReturnValue([DECONZ_CONBEE_II, EMBER_SKYCONNECT, ZSTACK_CC2538, ZBOSS_NORDIC, ZIGATE_PLUSV2]); + listSpy.mockReturnValue([DECONZ_CONBEE_II, EMBER_ZBDONGLE_E, ZSTACK_CC2538, ZBOSS_NORDIC, ZIGATE_PLUSV2]); platformSpy = jest.spyOn(os, 'platform'); platformSpy.mockReturnValue('linux'); }); it('detects deconz from scratch', async () => { - listSpy.mockReturnValue([DECONZ_CONBEE_II]); + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); @@ -194,20 +194,20 @@ describe('Adapter', () => { }); it('detects ember from scratch', async () => { - listSpy.mockReturnValue([EMBER_SKYCONNECT]); + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(EmberAdapter); // @ts-expect-error protected expect(adapter.serialPortOptions).toStrictEqual({ - path: EMBER_SKYCONNECT.path, + path: EMBER_ZBDONGLE_E.path, adapter: 'ember', }); }); it('detects zstack from scratch', async () => { - listSpy.mockReturnValue([ZSTACK_CC2538]); + listSpy.mockReturnValueOnce([ZSTACK_CC2538]); const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); @@ -220,7 +220,7 @@ describe('Adapter', () => { }); it('detects zboss from scratch', async () => { - listSpy.mockReturnValue([ZBOSS_NORDIC]); + listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); @@ -233,7 +233,7 @@ describe('Adapter', () => { }); it('detects zigate from scratch', async () => { - listSpy.mockReturnValue([ZIGATE_PLUSV2]); + listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); @@ -247,7 +247,7 @@ describe('Adapter', () => { it('detects from scratch on Windows', async () => { platformSpy.mockReturnValueOnce('win32'); - listSpy.mockReturnValue([ + listSpy.mockReturnValueOnce([ { // Windows sample - Sonoff Dongle-E path: 'COM3', @@ -272,7 +272,7 @@ describe('Adapter', () => { }); it('detects from scratch with pnpId', async () => { - listSpy.mockReturnValue([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); + listSpy.mockReturnValueOnce([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); @@ -285,7 +285,7 @@ describe('Adapter', () => { }); it('detects deconz with specific config', async () => { - listSpy.mockReturnValue([DECONZ_CONBEE_II]); + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); const adapter = await Adapter.create( {panID: 0x1a62, channelList: [11]}, @@ -303,11 +303,11 @@ describe('Adapter', () => { }); it('detects ember with specific config', async () => { - listSpy.mockReturnValue([EMBER_SKYCONNECT]); + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); const adapter = await Adapter.create( {panID: 0x1a62, channelList: [11]}, - {adapter: 'ember', path: EMBER_SKYCONNECT.path}, + {adapter: 'ember', path: EMBER_ZBDONGLE_E.path}, 'test.db.backup', {disableLED: false}, ); @@ -315,17 +315,17 @@ describe('Adapter', () => { expect(adapter).toBeInstanceOf(EmberAdapter); // @ts-expect-error protected expect(adapter.serialPortOptions).toStrictEqual({ - path: EMBER_SKYCONNECT.path, + path: EMBER_ZBDONGLE_E.path, adapter: 'ember', }); }); it('detects ezsp with specific config', async () => { - listSpy.mockReturnValue([EMBER_SKYCONNECT]); + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); const adapter = await Adapter.create( {panID: 0x1a62, channelList: [11]}, - {adapter: 'ezsp', path: EMBER_SKYCONNECT.path}, + {adapter: 'ezsp', path: EMBER_ZBDONGLE_E.path}, 'test.db.backup', {disableLED: false}, ); @@ -333,13 +333,13 @@ describe('Adapter', () => { expect(adapter).toBeInstanceOf(EZSPAdapter); // @ts-expect-error protected expect(adapter.serialPortOptions).toStrictEqual({ - path: EMBER_SKYCONNECT.path, + path: EMBER_ZBDONGLE_E.path, adapter: 'ezsp', }); }); it('detects zstack with specific config', async () => { - listSpy.mockReturnValue([ZSTACK_CC2538]); + listSpy.mockReturnValueOnce([ZSTACK_CC2538]); const adapter = await Adapter.create( {panID: 0x1a62, channelList: [11]}, @@ -357,7 +357,7 @@ describe('Adapter', () => { }); it('detects zboss with specific config', async () => { - listSpy.mockReturnValue([ZBOSS_NORDIC]); + listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss', path: ZBOSS_NORDIC.path}, 'test.db.backup', { disableLED: false, @@ -372,7 +372,7 @@ describe('Adapter', () => { }); it('detects zigate with specific config', async () => { - listSpy.mockReturnValue([ZIGATE_PLUSV2]); + listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); const adapter = await Adapter.create( {panID: 0x1a62, channelList: [11]}, @@ -390,7 +390,7 @@ describe('Adapter', () => { }); it('detects with specific config with multiple adapters connected', async () => { - listSpy.mockReturnValue([DECONZ_CONBEE_II, ZSTACK_CC2538, EMBER_SKYCONNECT]); + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II, ZSTACK_CC2538, EMBER_ZBDONGLE_E]); const adapter = await Adapter.create( {panID: 0x1a62, channelList: [11]}, @@ -408,7 +408,7 @@ describe('Adapter', () => { }); it('detects with specific config with pnpId', async () => { - listSpy.mockReturnValue([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); + listSpy.mockReturnValueOnce([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss', path: '/dev/ttyUSB0'}, 'test.db.backup', { disableLED: false, @@ -423,7 +423,7 @@ describe('Adapter', () => { }); it('fails to match specified adapter+path, tries to start anyway', async () => { - listSpy.mockReturnValue([]); + listSpy.mockReturnValueOnce([]); const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack', path: 'dev/ttyUSB0'}, 'test.db.backup', { disableLED: false, }); @@ -437,7 +437,7 @@ describe('Adapter', () => { }); it('fails to match with different paths, tries to start anyway', async () => { - listSpy.mockReturnValue([DECONZ_CONBEE_II]); + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'deconz', path: '/dev/ttyUSB0'}, 'test.db.backup', { disableLED: false, @@ -452,7 +452,7 @@ describe('Adapter', () => { }); it('fails to match from scratch with different paths, throws', async () => { - listSpy.mockReturnValue([DECONZ_CONBEE_II]); + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); expect(async () => { await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: '/dev/ttyUSB0'}, 'test.db.backup', {disableLED: false}); @@ -460,7 +460,13 @@ describe('Adapter', () => { }); it('fails to match with incomplete port info, throws', async () => { - listSpy.mockReturnValue([{...DECONZ_CONBEE_II, vendorId: undefined}]); + listSpy.mockReturnValueOnce([{...DECONZ_CONBEE_II, vendorId: undefined}]); + + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow(`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`); + + listSpy.mockReturnValueOnce([{...DECONZ_CONBEE_II, productId: undefined}]); expect(async () => { await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); @@ -468,7 +474,7 @@ describe('Adapter', () => { }); it('fails to match specified adapter+path, throws invalid adapter', async () => { - listSpy.mockReturnValue([]); + listSpy.mockReturnValueOnce([]); expect(async () => { await Adapter.create( @@ -514,5 +520,76 @@ describe('Adapter', () => { adapter: 'zstack', }); }); + + it('detects from scratch on conflict vendor+product IDs', async () => { + listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, manufacturer: undefined}]); + + let adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_SKYCONNECT.path, + adapter: 'ember', + }); + + listSpy.mockReturnValueOnce([{...ZSTACK_ZBDONGLE_P, path: '/dev/ttyACM0'}]); + + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyACM0', + adapter: 'zstack', + }); + }); + + it('fails to detect from scratch on conflict vendor+product IDs', async () => { + listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, path: '/dev/ttyACM0', manufacturer: undefined}]); + + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow(`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`); + }); + + it('detects with specific config on conflict vendor+product IDs', async () => { + listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, manufacturer: undefined}]); + + let adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'ember', path: EMBER_SKYCONNECT.path}, + 'test.db.backup', + {disableLED: false}, + ); + + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_SKYCONNECT.path, + adapter: 'ember', + }); + + listSpy.mockReturnValueOnce([{...ZSTACK_ZBDONGLE_P, path: '/dev/ttyACM0'}]); + + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack', path: '/dev/ttyACM0'}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyACM0', + adapter: 'zstack', + }); + }); + + it('fails to detect with adapter only on conflict vendor+product IDs', async () => { + listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, path: '/dev/ttyACM0', manufacturer: undefined}]); + + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack'}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow(`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`); + }); }); }); diff --git a/test/mockAdapters.ts b/test/mockAdapters.ts index 123de3dbb3..d70ce28683 100644 --- a/test/mockAdapters.ts +++ b/test/mockAdapters.ts @@ -4,9 +4,16 @@ export const DECONZ_CONBEE_II = { productId: '0030', manufacturer: 'dresden elektronik ingenieurtechnik GmbH', }; +export const EMBER_ZBDONGLE_E = { + path: '/dev/serial/by-id/usb-ITEAD_SONOFF_Zigbee_3.0_USB_Dongle_Plus_V2_20240122184111-if00', + vendorId: '1A86', // uppercased for extra coverage + productId: '55d4', + manufacturer: 'ITEAD', +}; +// vendorId+productId conflict with ZSTACK_ZBDONGLE_P export const EMBER_SKYCONNECT = { path: '/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_3abe54797c91ed118fc3cad13b20a111-if00-port0', - vendorId: '10C4', // uppercased for extra coverage + vendorId: '10c4', productId: 'ea60', manufacturer: 'Nabu Casa', }; @@ -16,6 +23,13 @@ export const ZSTACK_CC2538 = { productId: '16C8', // uppercased for extra coverage manufacturer: 'Texas Instruments', }; +// vendorId+productId conflict with EMBER_SKYCONNECT +export const ZSTACK_ZBDONGLE_P = { + path: '/dev/serial/by-id/usb-Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus_0111-if00-port0', + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'ITEAD', +}; export const ZBOSS_NORDIC = { path: '/dev/serial/by-id/usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00', vendorId: '2fe3', From 3ed35d7f2395e26d02bcc9dfaceb25711d3265d2 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Thu, 26 Sep 2024 00:45:37 +0200 Subject: [PATCH 08/13] Restrict search by config as much as possible. Reorganize tests, add more coverage. Cleanup. --- src/adapter/adapterDiscovery.ts | 40 +- test/adapter/adapter.test.ts | 841 +++++++++++++++++++++----------- 2 files changed, 575 insertions(+), 306 deletions(-) diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index e4cba05e7e..a1618dce14 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -259,8 +259,8 @@ function matchRegex(regexStr: string, str?: string): boolean { function matchUSBFingerprint( portInfo: PortInfo, - isWindows: boolean, entries: USBAdapterFingerprint[], + isWindows: boolean, conflictProne: boolean, ): [PortInfo['path'], USBAdapterFingerprint] | undefined { if (!portInfo.vendorId || !portInfo.productId) { @@ -285,6 +285,10 @@ function matchUSBFingerprint( (!entry.manufacturer || !portInfo.manufacturer || matchString(portInfo.manufacturer, entry.manufacturer) || isWindows) && (!entry.pathRegex || matchRegex(entry.pathRegex, portInfo.path) || matchRegex(entry.pathRegex, portInfo.pnpId) || isWindows) ) { + // if entry has either manufacturer or pathRegex, match as much as possible: + // - match manufacturer if available + // - try to match pathRegex against path or pnpId + // on Windows, allow fuzzier match, since manufacturer can get overridden by OS driver and path is COM return [portInfo.path, entry]; } } @@ -292,19 +296,22 @@ function matchUSBFingerprint( export async function matchUSBAdapter(adapter: ValidAdapter, path: string): Promise { const isWindows = platform() === 'win32'; + const portList = await getSerialPortList(); - for (const portInfo of await getSerialPortList()) { + logger.debug(() => `Connected devices: ${JSON.stringify(portList)}`, NS); + + for (const portInfo of portList) { /* istanbul ignore else */ if (portInfo.path !== path) { continue; } const conflictProne = USB_FINGERPRINTS_CONFLICT_IDS.includes(`${portInfo.vendorId}:${portInfo.productId}`); - const match = matchUSBFingerprint(portInfo, isWindows, USB_FINGERPRINTS[adapter === 'ezsp' ? 'ember' : adapter], conflictProne); + const match = matchUSBFingerprint(portInfo, USB_FINGERPRINTS[adapter === 'ezsp' ? 'ember' : adapter], isWindows, conflictProne); /* istanbul ignore else */ if (match) { - logger.info(`Matched adapter: ${JSON.stringify(portInfo)} => ${adapter}: ${JSON.stringify(match[1])}`, NS); + logger.info(() => `Matched adapter: ${JSON.stringify(portInfo)} => ${adapter}: ${JSON.stringify(match[1])}`, NS); return true; } } @@ -312,10 +319,18 @@ export async function matchUSBAdapter(adapter: ValidAdapter, path: string): Prom return false; } -export async function findUSBAdapter(path?: string): Promise<[adapter: DiscoverableUSBAdapter, path: PortInfo['path']] | undefined> { +export async function findUSBAdapter( + adapter?: ValidAdapter, + path?: string, +): Promise<[adapter: DiscoverableUSBAdapter, path: PortInfo['path']] | undefined> { const isWindows = platform() === 'win32'; + // refine to DiscoverableUSBAdapter + adapter = adapter && adapter === 'ezsp' ? 'ember' : adapter; + const portList = await getSerialPortList(); - for (const portInfo of await getSerialPortList()) { + logger.debug(() => `Connected devices: ${JSON.stringify(portList)}`, NS); + + for (const portInfo of portList) { if (path && portInfo.path !== path) { continue; } @@ -323,10 +338,14 @@ export async function findUSBAdapter(path?: string): Promise<[adapter: Discovera const conflictProne = USB_FINGERPRINTS_CONFLICT_IDS.includes(`${portInfo.vendorId}:${portInfo.productId}`); for (const key in USB_FINGERPRINTS) { - const match = matchUSBFingerprint(portInfo, isWindows, USB_FINGERPRINTS[key as DiscoverableUSBAdapter]!, conflictProne); + if (adapter && adapter !== key) { + continue; + } + + const match = matchUSBFingerprint(portInfo, USB_FINGERPRINTS[key as DiscoverableUSBAdapter]!, isWindows, conflictProne); if (match) { - logger.info(`Matched adapter: ${JSON.stringify(portInfo)} => ${key}: ${JSON.stringify(match[1])}`, NS); + logger.info(() => `Matched adapter: ${JSON.stringify(portInfo)} => ${key}: ${JSON.stringify(match[1])}`, NS); return [key as DiscoverableUSBAdapter, match[0]]; } } @@ -445,13 +464,14 @@ export async function discoverAdapter( try { // default to matching USB - const match = await findUSBAdapter(path); + const match = await findUSBAdapter(adapter && adapter !== 'auto' ? adapter : undefined, path); if (!match) { throw new Error(`No valid USB adapter found`); } - return match; + // keep adapter if `ezsp` since findUSBAdapter returns DiscoverableUSBAdapter + return adapter && adapter === 'ezsp' ? [adapter, match[1]] : match; } catch (error) { throw new Error(`USB adapter discovery error (${(error as Error).message}). Specify valid 'adapter' and 'port' in your configuration.`); } diff --git a/test/adapter/adapter.test.ts b/test/adapter/adapter.test.ts index 5f28ac1ef1..d86833c896 100644 --- a/test/adapter/adapter.test.ts +++ b/test/adapter/adapter.test.ts @@ -180,415 +180,664 @@ describe('Adapter', () => { platformSpy.mockReturnValue('linux'); }); - it('detects deconz from scratch', async () => { - listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); + describe('without config', () => { + it('detects deconz', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - expect(adapter).toBeInstanceOf(DeconzAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: DECONZ_CONBEE_II.path, - adapter: 'deconz', + expect(adapter).toBeInstanceOf(DeconzAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: DECONZ_CONBEE_II.path, + adapter: 'deconz', + }); }); - }); - it('detects ember from scratch', async () => { - listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); + it('detects ember', async () => { + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - expect(adapter).toBeInstanceOf(EmberAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: EMBER_ZBDONGLE_E.path, - adapter: 'ember', + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_ZBDONGLE_E.path, + adapter: 'ember', + }); }); - }); - it('detects zstack from scratch', async () => { - listSpy.mockReturnValueOnce([ZSTACK_CC2538]); + it('detects zstack', async () => { + listSpy.mockReturnValueOnce([ZSTACK_CC2538]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - expect(adapter).toBeInstanceOf(ZStackAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: ZSTACK_CC2538.path, - adapter: 'zstack', + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); }); - }); - it('detects zboss from scratch', async () => { - listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); + it('detects zboss', async () => { + listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - expect(adapter).toBeInstanceOf(ZBOSSAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: ZBOSS_NORDIC.path, - adapter: 'zboss', + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZBOSS_NORDIC.path, + adapter: 'zboss', + }); }); - }); - it('detects zigate from scratch', async () => { - listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); + it('detects zigate', async () => { + listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - expect(adapter).toBeInstanceOf(ZiGateAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: ZIGATE_PLUSV2.path, - adapter: 'zigate', + expect(adapter).toBeInstanceOf(ZiGateAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZIGATE_PLUSV2.path, + adapter: 'zigate', + }); }); - }); - it('detects from scratch on Windows', async () => { - platformSpy.mockReturnValueOnce('win32'); - listSpy.mockReturnValueOnce([ - { - // Windows sample - Sonoff Dongle-E + it('detects on Windows but less accurate', async () => { + platformSpy.mockReturnValueOnce('win32'); + listSpy.mockReturnValueOnce([ + { + // Windows sample - Sonoff Dongle-E + path: 'COM3', + manufacturer: 'wch.cn', + serialNumber: '54DD002111', + pnpId: 'USB\\VID_1A86&PID_55D4\\54DD002111', + locationId: 'Port_#0005.Hub_#0001', + friendlyName: 'USB-Enhanced-SERIAL CH9102 (COM3)', + vendorId: '1A86', + productId: '55D4', + }, + ]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ path: 'COM3', - manufacturer: 'wch.cn', - serialNumber: '54DD002111', - pnpId: 'USB\\VID_1A86&PID_55D4\\54DD002111', - locationId: 'Port_#0005.Hub_#0001', - friendlyName: 'USB-Enhanced-SERIAL CH9102 (COM3)', - vendorId: '1A86', - productId: '55D4', - }, - ]); + adapter: 'ember', + }); + }); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + it('detects with pnpId instead of path', async () => { + listSpy.mockReturnValueOnce([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); - expect(adapter).toBeInstanceOf(EmberAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: 'COM3', - adapter: 'ember', + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyUSB0', + adapter: 'zboss', + }); }); - }); - it('detects from scratch with pnpId', async () => { - listSpy.mockReturnValueOnce([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); + it('detects with conflict vendor+product IDs', async () => { + listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, manufacturer: undefined}]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + let adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - expect(adapter).toBeInstanceOf(ZBOSSAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: '/dev/ttyUSB0', - adapter: 'zboss', - }); - }); + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_SKYCONNECT.path, + adapter: 'ember', + }); - it('detects deconz with specific config', async () => { - listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); + listSpy.mockReturnValueOnce([{...ZSTACK_ZBDONGLE_P, path: '/dev/ttyACM0'}]); - const adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'deconz', path: DECONZ_CONBEE_II.path}, - 'test.db.backup', - {disableLED: false}, - ); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - expect(adapter).toBeInstanceOf(DeconzAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: DECONZ_CONBEE_II.path, - adapter: 'deconz', + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyACM0', + adapter: 'zstack', + }); }); - }); - it('detects ember with specific config', async () => { - listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); + it('returns first from list with multiple adapters - nothing to match against', async () => { + // NOTE: list is currently sorted + // const sortedPaths = [DECONZ_CONBEE_II.path, ZSTACK_CC2538.path, EMBER_ZBDONGLE_E.path].sort(); + // console.log(sortedPaths[0]); + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II, ZSTACK_CC2538, EMBER_ZBDONGLE_E]); - const adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'ember', path: EMBER_ZBDONGLE_E.path}, - 'test.db.backup', - {disableLED: false}, - ); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - expect(adapter).toBeInstanceOf(EmberAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: EMBER_ZBDONGLE_E.path, - adapter: 'ember', + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_ZBDONGLE_E.path, + adapter: 'ember', + }); }); - }); - it('detects ezsp with specific config', async () => { - listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); + it('throws on failure to get SerialPort.list', async () => { + listSpy.mockRejectedValueOnce(new Error('spawn udevadm ENOENT')); - const adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'ezsp', path: EMBER_ZBDONGLE_E.path}, - 'test.db.backup', - {disableLED: false}, - ); + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow(`USB adapter discovery error (spawn udevadm ENOENT). Specify valid 'adapter' and 'port' in your configuration.`); - expect(adapter).toBeInstanceOf(EZSPAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: EMBER_ZBDONGLE_E.path, - adapter: 'ezsp', - }); - }); + listSpy.mockRejectedValueOnce(new Error('spawn udevadm ENOENT')); - it('detects zstack with specific config', async () => { - listSpy.mockReturnValueOnce([ZSTACK_CC2538]); + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'auto'}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow(`USB adapter discovery error (spawn udevadm ENOENT). Specify valid 'adapter' and 'port' in your configuration.`); + }); - const adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'zstack', path: ZSTACK_CC2538.path}, - 'test.db.backup', - {disableLED: false}, - ); + it('throws on failure to detect with conflict vendor+product IDs', async () => { + listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, path: '/dev/ttyACM0', manufacturer: undefined}]); - expect(adapter).toBeInstanceOf(ZStackAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: ZSTACK_CC2538.path, - adapter: 'zstack', + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow( + `USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`, + ); }); }); - it('detects zboss with specific config', async () => { - listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); + describe('with adapter+path config', () => { + it('detects deconz', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss', path: ZBOSS_NORDIC.path}, 'test.db.backup', { - disableLED: false, - }); + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'deconz', path: DECONZ_CONBEE_II.path}, + 'test.db.backup', + {disableLED: false}, + ); - expect(adapter).toBeInstanceOf(ZBOSSAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: ZBOSS_NORDIC.path, - adapter: 'zboss', + expect(adapter).toBeInstanceOf(DeconzAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: DECONZ_CONBEE_II.path, + adapter: 'deconz', + }); }); - }); - it('detects zigate with specific config', async () => { - listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); + it('detects ember', async () => { + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); - const adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'zigate', path: ZIGATE_PLUSV2.path}, - 'test.db.backup', - {disableLED: false}, - ); + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'ember', path: EMBER_ZBDONGLE_E.path}, + 'test.db.backup', + {disableLED: false}, + ); - expect(adapter).toBeInstanceOf(ZiGateAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: ZIGATE_PLUSV2.path, - adapter: 'zigate', + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_ZBDONGLE_E.path, + adapter: 'ember', + }); }); - }); - it('detects with specific config with multiple adapters connected', async () => { - listSpy.mockReturnValueOnce([DECONZ_CONBEE_II, ZSTACK_CC2538, EMBER_ZBDONGLE_E]); + it('detects ezsp', async () => { + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); - const adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'zstack', path: ZSTACK_CC2538.path}, - 'test.db.backup', - {disableLED: false}, - ); + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'ezsp', path: EMBER_ZBDONGLE_E.path}, + 'test.db.backup', + {disableLED: false}, + ); - expect(adapter).toBeInstanceOf(ZStackAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: ZSTACK_CC2538.path, - adapter: 'zstack', + expect(adapter).toBeInstanceOf(EZSPAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_ZBDONGLE_E.path, + adapter: 'ezsp', + }); }); - }); - it('detects with specific config with pnpId', async () => { - listSpy.mockReturnValueOnce([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); + it('detects zstack', async () => { + listSpy.mockReturnValueOnce([ZSTACK_CC2538]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss', path: '/dev/ttyUSB0'}, 'test.db.backup', { - disableLED: false, - }); + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'zstack', path: ZSTACK_CC2538.path}, + 'test.db.backup', + {disableLED: false}, + ); - expect(adapter).toBeInstanceOf(ZBOSSAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: '/dev/ttyUSB0', - adapter: 'zboss', + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); }); - }); - it('fails to match specified adapter+path, tries to start anyway', async () => { - listSpy.mockReturnValueOnce([]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack', path: 'dev/ttyUSB0'}, 'test.db.backup', { - disableLED: false, + it('detects zboss', async () => { + listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); + + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'zboss', path: ZBOSS_NORDIC.path}, + 'test.db.backup', + { + disableLED: false, + }, + ); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZBOSS_NORDIC.path, + adapter: 'zboss', + }); }); - expect(adapter).toBeInstanceOf(ZStackAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: 'dev/ttyUSB0', - adapter: 'zstack', + it('detects zigate', async () => { + listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); + + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'zigate', path: ZIGATE_PLUSV2.path}, + 'test.db.backup', + {disableLED: false}, + ); + + expect(adapter).toBeInstanceOf(ZiGateAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZIGATE_PLUSV2.path, + adapter: 'zigate', + }); }); - }); - it('fails to match with different paths, tries to start anyway', async () => { - listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); + it('detects with multiple adapters connected', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II, ZSTACK_CC2538, EMBER_ZBDONGLE_E]); + + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'zstack', path: ZSTACK_CC2538.path}, + 'test.db.backup', + {disableLED: false}, + ); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'deconz', path: '/dev/ttyUSB0'}, 'test.db.backup', { - disableLED: false, + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); }); - expect(adapter).toBeInstanceOf(DeconzAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: '/dev/ttyUSB0', - adapter: 'deconz', + it('detects with pnpId instead of path', async () => { + listSpy.mockReturnValueOnce([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss', path: '/dev/ttyUSB0'}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyUSB0', + adapter: 'zboss', + }); }); - }); - it('fails to match from scratch with different paths, throws', async () => { - listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); + it('detects with conflict vendor+product IDs', async () => { + listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, manufacturer: undefined}]); - expect(async () => { - await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: '/dev/ttyUSB0'}, 'test.db.backup', {disableLED: false}); - }).rejects.toThrow(`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`); - }); + let adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'ember', path: EMBER_SKYCONNECT.path}, + 'test.db.backup', + {disableLED: false}, + ); - it('fails to match with incomplete port info, throws', async () => { - listSpy.mockReturnValueOnce([{...DECONZ_CONBEE_II, vendorId: undefined}]); + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_SKYCONNECT.path, + adapter: 'ember', + }); - expect(async () => { - await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - }).rejects.toThrow(`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`); + listSpy.mockReturnValueOnce([{...ZSTACK_ZBDONGLE_P, path: '/dev/ttyACM0'}]); - listSpy.mockReturnValueOnce([{...DECONZ_CONBEE_II, productId: undefined}]); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack', path: '/dev/ttyACM0'}, 'test.db.backup', { + disableLED: false, + }); - expect(async () => { - await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - }).rejects.toThrow(`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`); - }); + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyACM0', + adapter: 'zstack', + }); + }); - it('fails to match specified adapter+path, throws invalid adapter', async () => { - listSpy.mockReturnValueOnce([]); + it('returns instance anyway on failure to match', async () => { + listSpy.mockReturnValueOnce([]); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack', path: 'dev/ttyUSB0'}, 'test.db.backup', { + disableLED: false, + }); - expect(async () => { - await Adapter.create( + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: 'dev/ttyUSB0', + adapter: 'zstack', + }); + }); + + it('returns instance anyway on failure to match with different path', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); + + const adapter = await Adapter.create( {panID: 0x1a62, channelList: [11]}, - // @ts-expect-error invalid on purpose - {adapter: 'invalid', path: 'dev/ttyUSB0'}, + {adapter: 'deconz', path: '/dev/ttyUSB0'}, + 'test.db.backup', + { + disableLED: false, + }, + ); + + expect(adapter).toBeInstanceOf(DeconzAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyUSB0', + adapter: 'deconz', + }); + }); + + it('returns instance anyway on failure to get SerialPort.list', async () => { + listSpy.mockRejectedValueOnce(new Error('spawn udevadm ENOENT')); + + const adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'zstack', path: ZSTACK_CC2538.path}, 'test.db.backup', {disableLED: false}, ); - }).rejects.toThrow(`Adapter 'invalid' does not exists, possible options: zstack, deconz, zigate, ezsp, ember, zboss`); - }); - it('detecting from scratch fails to get SerialPort.list', async () => { - listSpy.mockRejectedValueOnce(new Error('spawn udevadm ENOENT')); + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); + }); - expect(async () => { - await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - }).rejects.toThrow(`USB adapter discovery error (spawn udevadm ENOENT). Specify valid 'adapter' and 'port' in your configuration.`); + it('throws on failure to match invalid adapter', async () => { + listSpy.mockReturnValueOnce([]); + + expect(async () => { + await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + // @ts-expect-error invalid on purpose + {adapter: 'invalid', path: 'dev/ttyUSB0'}, + 'test.db.backup', + {disableLED: false}, + ); + }).rejects.toThrow(`Adapter 'invalid' does not exists, possible options: zstack, deconz, zigate, ezsp, ember, zboss`); + }); }); - it('detecting with auto config fails to get SerialPort.list', async () => { - listSpy.mockRejectedValueOnce(new Error('spawn udevadm ENOENT')); + describe('with adapter only config', () => { + it('detects deconz', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); - expect(async () => { - await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'auto'}, 'test.db.backup', {disableLED: false}); - }).rejects.toThrow(`USB adapter discovery error (spawn udevadm ENOENT). Specify valid 'adapter' and 'port' in your configuration.`); - }); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'deconz'}, 'test.db.backup', {disableLED: false}); - it('detecting with specific config fails to get SerialPort.list, uses config anyway', async () => { - listSpy.mockRejectedValueOnce(new Error('spawn udevadm ENOENT')); + expect(adapter).toBeInstanceOf(DeconzAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: DECONZ_CONBEE_II.path, + adapter: 'deconz', + }); + }); - const adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'zstack', path: ZSTACK_CC2538.path}, - 'test.db.backup', - {disableLED: false}, - ); + it('detects ember', async () => { + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); - expect(adapter).toBeInstanceOf(ZStackAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: ZSTACK_CC2538.path, - adapter: 'zstack', + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'ember'}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_ZBDONGLE_E.path, + adapter: 'ember', + }); }); - }); - it('detects from scratch on conflict vendor+product IDs', async () => { - listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, manufacturer: undefined}]); + it('detects ezsp', async () => { + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); - let adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'ezsp'}, 'test.db.backup', {disableLED: false}); - expect(adapter).toBeInstanceOf(EmberAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: EMBER_SKYCONNECT.path, - adapter: 'ember', + expect(adapter).toBeInstanceOf(EZSPAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_ZBDONGLE_E.path, + adapter: 'ezsp', + }); }); - listSpy.mockReturnValueOnce([{...ZSTACK_ZBDONGLE_P, path: '/dev/ttyACM0'}]); + it('detects zstack', async () => { + listSpy.mockReturnValueOnce([ZSTACK_CC2538]); - adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack'}, 'test.db.backup', {disableLED: false}); - expect(adapter).toBeInstanceOf(ZStackAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: '/dev/ttyACM0', - adapter: 'zstack', + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); }); - }); - it('fails to detect from scratch on conflict vendor+product IDs', async () => { - listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, path: '/dev/ttyACM0', manufacturer: undefined}]); + it('detects zboss', async () => { + listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); - expect(async () => { - await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); - }).rejects.toThrow(`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss'}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZBOSS_NORDIC.path, + adapter: 'zboss', + }); + }); + + it('detects zigate', async () => { + listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zigate'}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZiGateAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZIGATE_PLUSV2.path, + adapter: 'zigate', + }); + }); + + it('detects with multiple adapters connected', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II, ZSTACK_CC2538, EMBER_ZBDONGLE_E]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack'}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); + }); + + it('detects with pnpId instead of path', async () => { + listSpy.mockReturnValueOnce([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss'}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyUSB0', + adapter: 'zboss', + }); + }); + + it('throws on failure to detect with conflict vendor+product IDs', async () => { + listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, path: '/dev/ttyACM0', manufacturer: undefined}]); + + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack'}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow( + `USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`, + ); + }); }); - it('detects with specific config on conflict vendor+product IDs', async () => { - listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, manufacturer: undefined}]); + describe('with path only config', () => { + it('detects deconz', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); - let adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'ember', path: EMBER_SKYCONNECT.path}, - 'test.db.backup', - {disableLED: false}, - ); + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: DECONZ_CONBEE_II.path}, 'test.db.backup', { + disableLED: false, + }); - expect(adapter).toBeInstanceOf(EmberAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: EMBER_SKYCONNECT.path, - adapter: 'ember', + expect(adapter).toBeInstanceOf(DeconzAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: DECONZ_CONBEE_II.path, + adapter: 'deconz', + }); }); - listSpy.mockReturnValueOnce([{...ZSTACK_ZBDONGLE_P, path: '/dev/ttyACM0'}]); + it('detects ember', async () => { + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: EMBER_ZBDONGLE_E.path}, 'test.db.backup', { + disableLED: false, + }); - adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack', path: '/dev/ttyACM0'}, 'test.db.backup', { - disableLED: false, + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_ZBDONGLE_E.path, + adapter: 'ember', + }); }); - expect(adapter).toBeInstanceOf(ZStackAdapter); - // @ts-expect-error protected - expect(adapter.serialPortOptions).toStrictEqual({ - path: '/dev/ttyACM0', - adapter: 'zstack', + it('detects zstack', async () => { + listSpy.mockReturnValueOnce([ZSTACK_CC2538]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: ZSTACK_CC2538.path}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); + }); + + it('detects zboss', async () => { + listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: ZBOSS_NORDIC.path}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZBOSS_NORDIC.path, + adapter: 'zboss', + }); + }); + + it('detects zigate', async () => { + listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: ZIGATE_PLUSV2.path}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(ZiGateAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZIGATE_PLUSV2.path, + adapter: 'zigate', + }); + }); + + it('detects with multiple adapters connected', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II, ZSTACK_CC2538, EMBER_ZBDONGLE_E]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: ZSTACK_CC2538.path}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); + }); + + it('detects with pnpId instead of path', async () => { + listSpy.mockReturnValueOnce([{...ZBOSS_NORDIC, path: '/dev/ttyUSB0', pnpId: 'usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00'}]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: '/dev/ttyUSB0'}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyUSB0', + adapter: 'zboss', + }); + }); + + it('throws on failure to match with different path', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); + + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: '/dev/ttyUSB0'}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow( + `USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`, + ); }); }); - it('fails to detect with adapter only on conflict vendor+product IDs', async () => { - listSpy.mockReturnValueOnce([{...EMBER_SKYCONNECT, path: '/dev/ttyACM0', manufacturer: undefined}]); + it('throws on failure to match when port info too limited', async () => { + listSpy.mockReturnValueOnce([{...DECONZ_CONBEE_II, vendorId: undefined}]); expect(async () => { - await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack'}, 'test.db.backup', {disableLED: false}); + await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow(`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`); + + listSpy.mockReturnValueOnce([{...DECONZ_CONBEE_II, productId: undefined}]); + + expect(async () => { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); }).rejects.toThrow(`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`); }); }); From 7b4cbb8b36043c4fe689a2e479ae8922779b1f5a Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:09:02 +0200 Subject: [PATCH 09/13] More fingerprints --- src/adapter/adapterDiscovery.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index a1618dce14..2feac0aca2 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -76,9 +76,10 @@ const USB_FINGERPRINTS: Record // SMLight slzb-07 vendorId: '10c4', productId: 'ea60', - // manufacturer: '', + manufacturer: 'SMLIGHT', + // /dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-07_be9faa0786e1ea11bd68dc2d9a583111-if00-port0 // /dev/serial/by-id/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_a215650c853bec119a079e957a0af111-if00-port0 - pathRegex: '.*slzb-07.*', + pathRegex: '.*slzb-07_.*', // `_` to not match 07p7 }, { // Sonoff ZBDongle-E V2 @@ -152,14 +153,14 @@ const USB_FINGERPRINTS: Record manufacturer: 'Texas Instruments', pathRegex: '.*CC26X2R1.*', // TODO }, - // { - // // TODO: SMLight slzb-07p7 - // vendorId: '', - // productId: '', - // // manufacturer: '', - // // /dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-07p7_be9faa0786e1ea11bd68dc2d9a583111-if00-port0 - // pathRegex: '.*SLZB-07p7.*', - // }, + { + // SMLight slzb-07p7 + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'SMLIGHT', + // /dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-07p7_be9faa0786e1ea11bd68dc2d9a583111-if00-port0 + pathRegex: '.*SLZB-07p7.*', + }, { // TubesZB ? vendorId: '10c4', From f0eacd33780ccf61cf13be08f3e6be59f5dcb86c Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:18:40 +0200 Subject: [PATCH 10/13] Add fingerprint for SMLight slzb-07mg24 --- src/adapter/adapterDiscovery.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index 2feac0aca2..4a8eab6fa4 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -81,6 +81,13 @@ const USB_FINGERPRINTS: Record // /dev/serial/by-id/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_a215650c853bec119a079e957a0af111-if00-port0 pathRegex: '.*slzb-07_.*', // `_` to not match 07p7 }, + { + // SMLight slzb-07mg24 + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'SMLIGHT', + pathRegex: '.*slzb-07mg24.*', + }, { // Sonoff ZBDongle-E V2 vendorId: '1a86', From e6083919964c077eea82ea64d961767fefa19b0e Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Mon, 30 Sep 2024 20:45:07 +0000 Subject: [PATCH 11/13] Add additional fingerprints + tests --- src/adapter/adapterDiscovery.ts | 34 +++++++++++++++++++++------------ test/adapter/adapter.test.ts | 24 ++++++++++++++++++++++- test/mockAdapters.ts | 18 +++++++++++++++-- 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index 4a8eab6fa4..47f112fe17 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -112,14 +112,15 @@ const USB_FINGERPRINTS: Record vendorId: '0403', productId: '6015', manufacturer: 'Electrolama', - pathRegex: '.*electrolame.*', // TODO + pathRegex: '.*electrolama.*', }, { // slae.sh cc2652rb vendorId: '10c4', productId: 'ea60', - // manufacturer: '', - pathRegex: '.*2652.*', + manufacturer: 'Silicon Labs', + // /dev/serial/by-id/usb-Silicon_Labs_slae.sh_cc2652rb_stick_-_slaesh_s_iot_stuff_00_12_4B_00_21_A8_EC_79-if00-port0 + pathRegex: '.*slae\.sh_cc2652rb.*', }, { // Sonoff ZBDongle-P (CC2652P) @@ -147,18 +148,11 @@ const USB_FINGERPRINTS: Record pathRegex: '.*CC2531.*', }, { - // CC1352P_2 - vendorId: '0451', - productId: 'bef3', - manufacturer: 'Texas Instruments', - pathRegex: '.*CC1352P_2.*', // TODO - }, - { - // CC26X2R1 + // Texas instruments launchpads vendorId: '0451', productId: 'bef3', manufacturer: 'Texas Instruments', - pathRegex: '.*CC26X2R1.*', // TODO + pathRegex: '.*Texas_Instruments.*', }, { // SMLight slzb-07p7 @@ -168,6 +162,22 @@ const USB_FINGERPRINTS: Record // /dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-07p7_be9faa0786e1ea11bd68dc2d9a583111-if00-port0 pathRegex: '.*SLZB-07p7.*', }, + { + // SMLight slzb-06p7 + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'SMLIGHT', + // /dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-06p7_82e43faf9872ed118bb924f3fdf7b791-if00-port0 + pathRegex: '.*SMLIGHT_SLZB-06p7_.*', + }, + { + // SMLight slzb-06p10 + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'SMLIGHT', + // /dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-06p10_40df2f3e3977ed11b142f6fafdf7b791-if00-port0 + pathRegex: '.*SMLIGHT_SLZB-06p10_.*', + }, { // TubesZB ? vendorId: '10c4', diff --git a/test/adapter/adapter.test.ts b/test/adapter/adapter.test.ts index d86833c896..0d0269062c 100644 --- a/test/adapter/adapter.test.ts +++ b/test/adapter/adapter.test.ts @@ -10,7 +10,7 @@ import {SerialPort} from '../../src/adapter/serialPort'; import {ZStackAdapter} from '../../src/adapter/z-stack/adapter'; import {ZBOSSAdapter} from '../../src/adapter/zboss/adapter'; import {ZiGateAdapter} from '../../src/adapter/zigate/adapter'; -import {DECONZ_CONBEE_II, EMBER_SKYCONNECT, EMBER_ZBDONGLE_E, ZBOSS_NORDIC, ZIGATE_PLUSV2, ZSTACK_CC2538, ZSTACK_ZBDONGLE_P} from '../mockAdapters'; +import {DECONZ_CONBEE_II, EMBER_SKYCONNECT, EMBER_ZBDONGLE_E, ZBOSS_NORDIC, ZIGATE_PLUSV2, ZSTACK_CC2538, ZSTACK_SMLIGHT_SLZB_06P10, ZSTACK_SMLIGHT_SLZB_07, ZSTACK_ZBDONGLE_P} from '../mockAdapters'; const mockBonjourResult = jest.fn().mockImplementation((type) => ({ name: 'Mock Adapter', @@ -307,6 +307,28 @@ describe('Adapter', () => { path: '/dev/ttyACM0', adapter: 'zstack', }); + + listSpy.mockReturnValueOnce([ZSTACK_SMLIGHT_SLZB_06P10]); + + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_SMLIGHT_SLZB_06P10.path, + adapter: 'zstack', + }); + + listSpy.mockReturnValueOnce([ZSTACK_SMLIGHT_SLZB_07]); + + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_SMLIGHT_SLZB_07.path, + adapter: 'ember', + }); }); it('returns first from list with multiple adapters - nothing to match against', async () => { diff --git a/test/mockAdapters.ts b/test/mockAdapters.ts index d70ce28683..aa5ff84b16 100644 --- a/test/mockAdapters.ts +++ b/test/mockAdapters.ts @@ -10,7 +10,7 @@ export const EMBER_ZBDONGLE_E = { productId: '55d4', manufacturer: 'ITEAD', }; -// vendorId+productId conflict with ZSTACK_ZBDONGLE_P +// vendorId+productId conflict with all 10c4:ea60 export const EMBER_SKYCONNECT = { path: '/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_3abe54797c91ed118fc3cad13b20a111-if00-port0', vendorId: '10c4', @@ -23,13 +23,27 @@ export const ZSTACK_CC2538 = { productId: '16C8', // uppercased for extra coverage manufacturer: 'Texas Instruments', }; -// vendorId+productId conflict with EMBER_SKYCONNECT +// vendorId+productId conflict with all 10c4:ea60 export const ZSTACK_ZBDONGLE_P = { path: '/dev/serial/by-id/usb-Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus_0111-if00-port0', vendorId: '10c4', productId: 'ea60', manufacturer: 'ITEAD', }; +// vendorId+productId conflict with all 10c4:ea60 +export const ZSTACK_SMLIGHT_SLZB_06P10 = { + path: '/dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-06p10_40df2f3e3977ed11b142f6fafdf7b791-if00-port0', + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'SMLIGHT', +}; +// vendorId+productId conflict with all 10c4:ea60 +export const ZSTACK_SMLIGHT_SLZB_07 = { + path: '/dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-07_be9faa0786e1ea11bd68dc2d9a583111-if00-port0', + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'SMLIGHT', +}; export const ZBOSS_NORDIC = { path: '/dev/serial/by-id/usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00', vendorId: '2fe3', From 27eeee655a5dbd133059b96c83da87b17d3fd799 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:49:47 +0200 Subject: [PATCH 12/13] Better matching with scoring. --- src/adapter/adapterDiscovery.ts | 85 +++++++++++++---- test/adapter/adapter.test.ts | 159 +++++++++++++++----------------- 2 files changed, 138 insertions(+), 106 deletions(-) diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index 47f112fe17..bb519cd3a9 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -9,6 +9,14 @@ import {Adapter, DiscoverableUSBAdapter, USBAdapterFingerprint, ValidAdapter} fr const NS = 'zh:adapter:discovery'; +const enum USBFingerprintMatchScore { + NONE = 0, + VID_PID = 1, + VID_PID_MANUF = 2, + VID_PID_PATH = 3, + VID_PID_MANUF_PATH = 4, +} + /** * @see https://serialport.io/docs/api-bindings-cpp#list * @@ -120,7 +128,7 @@ const USB_FINGERPRINTS: Record productId: 'ea60', manufacturer: 'Silicon Labs', // /dev/serial/by-id/usb-Silicon_Labs_slae.sh_cc2652rb_stick_-_slaesh_s_iot_stuff_00_12_4B_00_21_A8_EC_79-if00-port0 - pathRegex: '.*slae\.sh_cc2652rb.*', + pathRegex: '.*slae\\.sh_cc2652rb.*', }, { // Sonoff ZBDongle-P (CC2652P) @@ -280,36 +288,59 @@ function matchUSBFingerprint( entries: USBAdapterFingerprint[], isWindows: boolean, conflictProne: boolean, -): [PortInfo['path'], USBAdapterFingerprint] | undefined { +): [path: PortInfo['path'], score: number] | undefined { if (!portInfo.vendorId || !portInfo.productId) { // port info is missing essential information for proper matching, ignore it return; } + let match: USBAdapterFingerprint | undefined; + let score: number = USBFingerprintMatchScore.NONE; + for (const entry of entries) { if (!matchString(portInfo.vendorId, entry.vendorId) || !matchString(portInfo.productId, entry.productId)) { continue; } - if (conflictProne) { - // if vendor+product combo is conflict prone, enforce at least one of manufacturer or pathRegex to match to avoid false positive - if ( - (entry.manufacturer && portInfo.manufacturer && matchString(portInfo.manufacturer, entry.manufacturer)) || - (entry.pathRegex && (matchRegex(entry.pathRegex, portInfo.path) || matchRegex(entry.pathRegex, portInfo.pnpId))) - ) { - return [portInfo.path, entry]; + // allow matching on vendorId+productId only on Windows + if (score < USBFingerprintMatchScore.VID_PID && isWindows) { + match = entry; + score = USBFingerprintMatchScore.VID_PID; + } + + if ( + score < USBFingerprintMatchScore.VID_PID_MANUF && + entry.manufacturer && + portInfo.manufacturer && + matchString(portInfo.manufacturer, entry.manufacturer) + ) { + match = entry; + score = USBFingerprintMatchScore.VID_PID_MANUF; + + if (isWindows && !conflictProne) { + // path will never match on Windows (COMx), assume vendor+product+manufacturer is "exact match" + // except for conflict-prone, since it could easily return a mismatch (better to return no match and force manual config) + return [portInfo.path, score]; } - } else if ( - (!entry.manufacturer || !portInfo.manufacturer || matchString(portInfo.manufacturer, entry.manufacturer) || isWindows) && - (!entry.pathRegex || matchRegex(entry.pathRegex, portInfo.path) || matchRegex(entry.pathRegex, portInfo.pnpId) || isWindows) + } + + if ( + score < USBFingerprintMatchScore.VID_PID_PATH && + entry.pathRegex && + (matchRegex(entry.pathRegex, portInfo.path) || matchRegex(entry.pathRegex, portInfo.pnpId)) ) { - // if entry has either manufacturer or pathRegex, match as much as possible: - // - match manufacturer if available - // - try to match pathRegex against path or pnpId - // on Windows, allow fuzzier match, since manufacturer can get overridden by OS driver and path is COM - return [portInfo.path, entry]; + if (score === USBFingerprintMatchScore.VID_PID_MANUF) { + // best possible match, return early + return [portInfo.path, USBFingerprintMatchScore.VID_PID_MANUF_PATH]; + } else { + match = entry; + score = USBFingerprintMatchScore.VID_PID_PATH; + } } } + + // poor match only returned if port info not conflict-prone + return match && (score > USBFingerprintMatchScore.VID_PID || !conflictProne) ? [portInfo.path, score] : undefined; } export async function matchUSBAdapter(adapter: ValidAdapter, path: string): Promise { @@ -354,6 +385,7 @@ export async function findUSBAdapter( } const conflictProne = USB_FINGERPRINTS_CONFLICT_IDS.includes(`${portInfo.vendorId}:${portInfo.productId}`); + let bestMatch: [DiscoverableUSBAdapter, NonNullable>] | undefined; for (const key in USB_FINGERPRINTS) { if (adapter && adapter !== key) { @@ -362,11 +394,24 @@ export async function findUSBAdapter( const match = matchUSBFingerprint(portInfo, USB_FINGERPRINTS[key as DiscoverableUSBAdapter]!, isWindows, conflictProne); - if (match) { - logger.info(() => `Matched adapter: ${JSON.stringify(portInfo)} => ${key}: ${JSON.stringify(match[1])}`, NS); - return [key as DiscoverableUSBAdapter, match[0]]; + // register the match if no previous or better score + if (match && (!bestMatch || bestMatch[1][1] < match[1])) { + bestMatch = [key as DiscoverableUSBAdapter, match]; + + if (match[1] === USBFingerprintMatchScore.VID_PID_MANUF_PATH) { + // got best possible match, exit loop + break; + } } } + + if (bestMatch) { + logger.info( + () => `Matched adapter: ${JSON.stringify(portInfo)} => ${bestMatch[0]}: path=${bestMatch[1][0]}, score=${bestMatch[1][1]}`, + NS, + ); + return [bestMatch[0], bestMatch[1][0]]; + } } } diff --git a/test/adapter/adapter.test.ts b/test/adapter/adapter.test.ts index 0d0269062c..e226028b7e 100644 --- a/test/adapter/adapter.test.ts +++ b/test/adapter/adapter.test.ts @@ -10,7 +10,17 @@ import {SerialPort} from '../../src/adapter/serialPort'; import {ZStackAdapter} from '../../src/adapter/z-stack/adapter'; import {ZBOSSAdapter} from '../../src/adapter/zboss/adapter'; import {ZiGateAdapter} from '../../src/adapter/zigate/adapter'; -import {DECONZ_CONBEE_II, EMBER_SKYCONNECT, EMBER_ZBDONGLE_E, ZBOSS_NORDIC, ZIGATE_PLUSV2, ZSTACK_CC2538, ZSTACK_SMLIGHT_SLZB_06P10, ZSTACK_SMLIGHT_SLZB_07, ZSTACK_ZBDONGLE_P} from '../mockAdapters'; +import { + DECONZ_CONBEE_II, + EMBER_SKYCONNECT, + EMBER_ZBDONGLE_E, + ZBOSS_NORDIC, + ZIGATE_PLUSV2, + ZSTACK_CC2538, + ZSTACK_SMLIGHT_SLZB_06P10, + ZSTACK_SMLIGHT_SLZB_07, + ZSTACK_ZBDONGLE_P, +} from '../mockAdapters'; const mockBonjourResult = jest.fn().mockImplementation((type) => ({ name: 'Mock Adapter', @@ -181,10 +191,10 @@ describe('Adapter', () => { }); describe('without config', () => { - it('detects deconz', async () => { + it('detects each adapter', async () => { listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + let adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(DeconzAdapter); // @ts-expect-error protected @@ -192,12 +202,10 @@ describe('Adapter', () => { path: DECONZ_CONBEE_II.path, adapter: 'deconz', }); - }); - it('detects ember', async () => { listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(EmberAdapter); // @ts-expect-error protected @@ -205,12 +213,10 @@ describe('Adapter', () => { path: EMBER_ZBDONGLE_E.path, adapter: 'ember', }); - }); - it('detects zstack', async () => { listSpy.mockReturnValueOnce([ZSTACK_CC2538]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(ZStackAdapter); // @ts-expect-error protected @@ -218,12 +224,10 @@ describe('Adapter', () => { path: ZSTACK_CC2538.path, adapter: 'zstack', }); - }); - it('detects zboss', async () => { listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(ZBOSSAdapter); // @ts-expect-error protected @@ -231,12 +235,10 @@ describe('Adapter', () => { path: ZBOSS_NORDIC.path, adapter: 'zboss', }); - }); - it('detects zigate', async () => { listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(ZiGateAdapter); // @ts-expect-error protected @@ -246,7 +248,34 @@ describe('Adapter', () => { }); }); - it('detects on Windows but less accurate', async () => { + it('detects on Windows with manufacturer present', async () => { + platformSpy.mockReturnValueOnce('win32'); + listSpy.mockReturnValueOnce([ + { + // Windows sample - Sonoff Dongle-E + path: 'COM3', + manufacturer: 'ITEAD', + serialNumber: '54DD002111', + pnpId: 'USB\\VID_1A86&PID_55D4\\54DD002111', + locationId: 'Port_#0005.Hub_#0001', + friendlyName: 'USB-Enhanced-SERIAL CH9102 (COM3)', + vendorId: '1A86', + productId: '55D4', + }, + ]); + + const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: 'COM3', + adapter: 'ember', + }); + }); + + it('detects on Windows without manufacturer present', async () => { + // Note: this is the least-accurate possible match platformSpy.mockReturnValueOnce('win32'); listSpy.mockReturnValueOnce([ { @@ -373,10 +402,10 @@ describe('Adapter', () => { }); describe('with adapter+path config', () => { - it('detects deconz', async () => { + it('detects each adapter', async () => { listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); - const adapter = await Adapter.create( + let adapter = await Adapter.create( {panID: 0x1a62, channelList: [11]}, {adapter: 'deconz', path: DECONZ_CONBEE_II.path}, 'test.db.backup', @@ -389,12 +418,10 @@ describe('Adapter', () => { path: DECONZ_CONBEE_II.path, adapter: 'deconz', }); - }); - it('detects ember', async () => { listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); - const adapter = await Adapter.create( + adapter = await Adapter.create( {panID: 0x1a62, channelList: [11]}, {adapter: 'ember', path: EMBER_ZBDONGLE_E.path}, 'test.db.backup', @@ -407,17 +434,12 @@ describe('Adapter', () => { path: EMBER_ZBDONGLE_E.path, adapter: 'ember', }); - }); - it('detects ezsp', async () => { listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); - const adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'ezsp', path: EMBER_ZBDONGLE_E.path}, - 'test.db.backup', - {disableLED: false}, - ); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'ezsp', path: EMBER_ZBDONGLE_E.path}, 'test.db.backup', { + disableLED: false, + }); expect(adapter).toBeInstanceOf(EZSPAdapter); // @ts-expect-error protected @@ -425,17 +447,12 @@ describe('Adapter', () => { path: EMBER_ZBDONGLE_E.path, adapter: 'ezsp', }); - }); - it('detects zstack', async () => { listSpy.mockReturnValueOnce([ZSTACK_CC2538]); - const adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'zstack', path: ZSTACK_CC2538.path}, - 'test.db.backup', - {disableLED: false}, - ); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack', path: ZSTACK_CC2538.path}, 'test.db.backup', { + disableLED: false, + }); expect(adapter).toBeInstanceOf(ZStackAdapter); // @ts-expect-error protected @@ -443,19 +460,12 @@ describe('Adapter', () => { path: ZSTACK_CC2538.path, adapter: 'zstack', }); - }); - it('detects zboss', async () => { listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); - const adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'zboss', path: ZBOSS_NORDIC.path}, - 'test.db.backup', - { - disableLED: false, - }, - ); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss', path: ZBOSS_NORDIC.path}, 'test.db.backup', { + disableLED: false, + }); expect(adapter).toBeInstanceOf(ZBOSSAdapter); // @ts-expect-error protected @@ -463,17 +473,12 @@ describe('Adapter', () => { path: ZBOSS_NORDIC.path, adapter: 'zboss', }); - }); - it('detects zigate', async () => { listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); - const adapter = await Adapter.create( - {panID: 0x1a62, channelList: [11]}, - {adapter: 'zigate', path: ZIGATE_PLUSV2.path}, - 'test.db.backup', - {disableLED: false}, - ); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zigate', path: ZIGATE_PLUSV2.path}, 'test.db.backup', { + disableLED: false, + }); expect(adapter).toBeInstanceOf(ZiGateAdapter); // @ts-expect-error protected @@ -615,10 +620,10 @@ describe('Adapter', () => { }); describe('with adapter only config', () => { - it('detects deconz', async () => { + it('detects each adapter', async () => { listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'deconz'}, 'test.db.backup', {disableLED: false}); + let adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'deconz'}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(DeconzAdapter); // @ts-expect-error protected @@ -626,12 +631,10 @@ describe('Adapter', () => { path: DECONZ_CONBEE_II.path, adapter: 'deconz', }); - }); - it('detects ember', async () => { listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'ember'}, 'test.db.backup', {disableLED: false}); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'ember'}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(EmberAdapter); // @ts-expect-error protected @@ -639,12 +642,10 @@ describe('Adapter', () => { path: EMBER_ZBDONGLE_E.path, adapter: 'ember', }); - }); - it('detects ezsp', async () => { listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'ezsp'}, 'test.db.backup', {disableLED: false}); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'ezsp'}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(EZSPAdapter); // @ts-expect-error protected @@ -652,12 +653,10 @@ describe('Adapter', () => { path: EMBER_ZBDONGLE_E.path, adapter: 'ezsp', }); - }); - it('detects zstack', async () => { listSpy.mockReturnValueOnce([ZSTACK_CC2538]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack'}, 'test.db.backup', {disableLED: false}); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zstack'}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(ZStackAdapter); // @ts-expect-error protected @@ -665,12 +664,10 @@ describe('Adapter', () => { path: ZSTACK_CC2538.path, adapter: 'zstack', }); - }); - it('detects zboss', async () => { listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss'}, 'test.db.backup', {disableLED: false}); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zboss'}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(ZBOSSAdapter); // @ts-expect-error protected @@ -678,12 +675,10 @@ describe('Adapter', () => { path: ZBOSS_NORDIC.path, adapter: 'zboss', }); - }); - it('detects zigate', async () => { listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zigate'}, 'test.db.backup', {disableLED: false}); + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'zigate'}, 'test.db.backup', {disableLED: false}); expect(adapter).toBeInstanceOf(ZiGateAdapter); // @ts-expect-error protected @@ -733,10 +728,10 @@ describe('Adapter', () => { }); describe('with path only config', () => { - it('detects deconz', async () => { + it('detects each adapter', async () => { listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: DECONZ_CONBEE_II.path}, 'test.db.backup', { + let adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: DECONZ_CONBEE_II.path}, 'test.db.backup', { disableLED: false, }); @@ -746,12 +741,10 @@ describe('Adapter', () => { path: DECONZ_CONBEE_II.path, adapter: 'deconz', }); - }); - it('detects ember', async () => { listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: EMBER_ZBDONGLE_E.path}, 'test.db.backup', { + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: EMBER_ZBDONGLE_E.path}, 'test.db.backup', { disableLED: false, }); @@ -761,12 +754,10 @@ describe('Adapter', () => { path: EMBER_ZBDONGLE_E.path, adapter: 'ember', }); - }); - it('detects zstack', async () => { listSpy.mockReturnValueOnce([ZSTACK_CC2538]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: ZSTACK_CC2538.path}, 'test.db.backup', { + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: ZSTACK_CC2538.path}, 'test.db.backup', { disableLED: false, }); @@ -776,12 +767,10 @@ describe('Adapter', () => { path: ZSTACK_CC2538.path, adapter: 'zstack', }); - }); - it('detects zboss', async () => { listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: ZBOSS_NORDIC.path}, 'test.db.backup', { + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: ZBOSS_NORDIC.path}, 'test.db.backup', { disableLED: false, }); @@ -791,12 +780,10 @@ describe('Adapter', () => { path: ZBOSS_NORDIC.path, adapter: 'zboss', }); - }); - it('detects zigate', async () => { listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); - const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: ZIGATE_PLUSV2.path}, 'test.db.backup', { + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: ZIGATE_PLUSV2.path}, 'test.db.backup', { disableLED: false, }); From ecbbe1281003e48a47eef92e3d797fcc4f6bbcbe Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:18:03 +0200 Subject: [PATCH 13/13] Remove `auto` from Adapter type. --- src/adapter/adapterDiscovery.ts | 27 ++++++++++----------------- src/adapter/tstype.ts | 3 +-- test/adapter/adapter.test.ts | 25 +------------------------ 3 files changed, 12 insertions(+), 43 deletions(-) diff --git a/src/adapter/adapterDiscovery.ts b/src/adapter/adapterDiscovery.ts index bb519cd3a9..a2805ba2d2 100644 --- a/src/adapter/adapterDiscovery.ts +++ b/src/adapter/adapterDiscovery.ts @@ -5,7 +5,7 @@ import {Bonjour, Service} from 'bonjour-service'; import {logger} from '../utils/logger'; import {SerialPort} from './serialPort'; -import {Adapter, DiscoverableUSBAdapter, USBAdapterFingerprint, ValidAdapter} from './tstype'; +import {Adapter, DiscoverableUSBAdapter, USBAdapterFingerprint} from './tstype'; const NS = 'zh:adapter:discovery'; @@ -343,7 +343,7 @@ function matchUSBFingerprint( return match && (score > USBFingerprintMatchScore.VID_PID || !conflictProne) ? [portInfo.path, score] : undefined; } -export async function matchUSBAdapter(adapter: ValidAdapter, path: string): Promise { +export async function matchUSBAdapter(adapter: Adapter, path: string): Promise { const isWindows = platform() === 'win32'; const portList = await getSerialPortList(); @@ -369,7 +369,7 @@ export async function matchUSBAdapter(adapter: ValidAdapter, path: string): Prom } export async function findUSBAdapter( - adapter?: ValidAdapter, + adapter?: Adapter, path?: string, ): Promise<[adapter: DiscoverableUSBAdapter, path: PortInfo['path']] | undefined> { const isWindows = platform() === 'win32'; @@ -415,7 +415,7 @@ export async function findUSBAdapter( } } -export async function findmDNSAdapter(path: string): Promise<[adapter: ValidAdapter, path: string, baudRate: number]> { +export async function findmDNSAdapter(path: string): Promise<[adapter: Adapter, path: string, baudRate: number]> { const mdnsDevice = path.substring(7); if (mdnsDevice.length == 0) { @@ -446,11 +446,7 @@ export async function findmDNSAdapter(path: string): Promise<[adapter: ValidAdap const adapter = mdnsAdapter; const baudRate = mdnsBaud; - if (adapter && adapter !== 'auto') { - resolve([adapter, path, baudRate]); - } else { - reject(new Error(`Adapter ${adapter} is not supported.`)); - } + resolve([adapter, path, baudRate]); } else { bj.destroy(); reject( @@ -471,14 +467,14 @@ export async function findmDNSAdapter(path: string): Promise<[adapter: ValidAdap }); } -export async function findTCPAdapter(path: string, adapter?: Adapter): Promise<[adapter: ValidAdapter, path: string]> { +export async function findTCPAdapter(path: string, adapter?: Adapter): Promise<[adapter: Adapter, path: string]> { const regex = /^tcp:\/\/(?:[0-9]{1,3}\.){3}[0-9]{1,3}:\d{1,5}$/gm; if (!regex.test(path)) { throw new Error(`Invalid TCP path, expected format: tcp://:`); } - if (!adapter || adapter === 'auto') { + if (!adapter) { throw new Error(`Cannot discover TCP adapters at this time. Specify valid 'adapter' and 'port' in your configuration.`); } @@ -500,16 +496,13 @@ export async function findTCPAdapter(path: string, adapter?: Adapter): Promise<[ * @returns path Path to adapter. * @returns baudRate [optional] Discovered baud rate of the adapter. Valid only for mDNS discovery at the moment. */ -export async function discoverAdapter( - adapter?: Adapter, - path?: string, -): Promise<[adapter: ValidAdapter, path: string, baudRate?: number | undefined]> { +export async function discoverAdapter(adapter?: Adapter, path?: string): Promise<[adapter: Adapter, path: string, baudRate?: number | undefined]> { if (path) { if (path.startsWith('mdns://')) { return await findmDNSAdapter(path); } else if (path.startsWith('tcp://')) { return await findTCPAdapter(path, adapter); - } else if (adapter && adapter !== 'auto') { + } else if (adapter) { try { const matched = await matchUSBAdapter(adapter, path); @@ -527,7 +520,7 @@ export async function discoverAdapter( try { // default to matching USB - const match = await findUSBAdapter(adapter && adapter !== 'auto' ? adapter : undefined, path); + const match = await findUSBAdapter(adapter, path); if (!match) { throw new Error(`No valid USB adapter found`); diff --git a/src/adapter/tstype.ts b/src/adapter/tstype.ts index 22f38817af..e7402a2460 100644 --- a/src/adapter/tstype.ts +++ b/src/adapter/tstype.ts @@ -1,5 +1,4 @@ -export type Adapter = 'auto' | 'deconz' | 'ember' | 'zstack' | 'zboss' | 'zigate' | 'ezsp'; -export type ValidAdapter = 'deconz' | 'ember' | 'zstack' | 'zboss' | 'zigate' | 'ezsp'; +export type Adapter = 'deconz' | 'ember' | 'zstack' | 'zboss' | 'zigate' | 'ezsp'; export type DiscoverableUSBAdapter = 'deconz' | 'ember' | 'zstack' | 'zboss' | 'zigate'; export type USBAdapterFingerprint = { diff --git a/test/adapter/adapter.test.ts b/test/adapter/adapter.test.ts index e226028b7e..f59057a471 100644 --- a/test/adapter/adapter.test.ts +++ b/test/adapter/adapter.test.ts @@ -126,23 +126,6 @@ describe('Adapter', () => { `port, got: 1122`, ); }); - - it('returns auto adapter', async () => { - mockBonjourResult.mockReturnValueOnce({ - name: 'Mock Adapter', - type: `my_adapter_mdns`, - port: '1122', - addresses: ['192.168.1.123'], - txt: { - radio_type: 'auto', - baud_rate: 115200, - }, - }); - - expect(async () => { - await Adapter.create({panID: 0, channelList: []}, {path: `mdns://my_adapter`}, 'test.db', {disableLED: false}); - }).rejects.toThrow(`Adapter auto is not supported.`); - }); }); describe('TCP discovery', () => { @@ -171,7 +154,7 @@ describe('Adapter', () => { it('invalid adapter', async () => { expect(async () => { - await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: `tcp://192.168.1.321:3456`, adapter: `auto`}, 'test.db.backup', { + await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: `tcp://192.168.1.321:3456`}, 'test.db.backup', { disableLED: false, }); }).rejects.toThrow(`Cannot discover TCP adapters at this time. Specify valid 'adapter' and 'port' in your configuration.`); @@ -382,12 +365,6 @@ describe('Adapter', () => { expect(async () => { await Adapter.create({panID: 0x1a62, channelList: [11]}, {}, 'test.db.backup', {disableLED: false}); }).rejects.toThrow(`USB adapter discovery error (spawn udevadm ENOENT). Specify valid 'adapter' and 'port' in your configuration.`); - - listSpy.mockRejectedValueOnce(new Error('spawn udevadm ENOENT')); - - expect(async () => { - await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'auto'}, 'test.db.backup', {disableLED: false}); - }).rejects.toThrow(`USB adapter discovery error (spawn udevadm ENOENT). Specify valid 'adapter' and 'port' in your configuration.`); }); it('throws on failure to detect with conflict vendor+product IDs', async () => {