From 1d35a1435512285323b0dd0e185ffdbe23ac05b9 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Fri, 11 Oct 2024 22:10:13 +0200 Subject: [PATCH] feat!: Improved adapter discovery (#1197) * feat: Improved adapter discovery. * Fix. * Update + tests. * Add alt regex matching on `pnpId`. * Feedback. * Lower log level on specified config matching fail. * Improve matching logic to avoid false positive. * Restrict search by config as much as possible. Reorganize tests, add more coverage. Cleanup. * More fingerprints * Add fingerprint for SMLight slzb-07mg24 * Add additional fingerprints + tests * Better matching with scoring. * Remove `auto` from Adapter type. --------- Co-authored-by: Koen Kanters --- src/adapter/adapter.ts | 122 +-- src/adapter/adapterDiscovery.ts | 534 ++++++++++++ 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 | 51 +- 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 | 830 +++++++++++++++++++ test/adapter/z-stack/adapter.test.ts | 14 - test/adapter/z-stack/znp.test.ts | 56 -- test/controller.test.ts | 443 +--------- test/mockAdapters.ts | 57 ++ 20 files changed, 1463 insertions(+), 861 deletions(-) create mode 100644 src/adapter/adapterDiscovery.ts 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 new file mode 100644 index 0000000000..a2805ba2d2 --- /dev/null +++ b/src/adapter/adapterDiscovery.ts @@ -0,0 +1,534 @@ +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 {Adapter, DiscoverableUSBAdapter, USBAdapterFingerprint} from './tstype'; + +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 + * + * 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_FINGERPRINTS: 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: '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_.*', // `_` to not match 07p7 + }, + { + // SMLight slzb-07mg24 + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'SMLIGHT', + pathRegex: '.*slzb-07mg24.*', + }, + { + // 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 + // /dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_186ff44314e2ed11b891eb5162c61111-if00-port0 + 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: '.*electrolama.*', + }, + { + // slae.sh cc2652rb + vendorId: '10c4', + 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.*', + }, + { + // Sonoff ZBDongle-P (CC2652P) + vendorId: '10c4', + productId: 'ea60', + 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.*', + }, + { + // 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.*', + }, + { + // Texas instruments launchpads + vendorId: '0451', + productId: 'bef3', + manufacturer: 'Texas Instruments', + pathRegex: '.*Texas_Instruments.*', + }, + { + // SMLight slzb-07p7 + vendorId: '10c4', + productId: 'ea60', + manufacturer: 'SMLIGHT', + // /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', + 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', + // /dev/serial/by-id/usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DAD111-if00 + pathRegex: '.*ZEPHYR.*', + }, + ], + 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.*', + }, + ], +}; + +/** + * 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(); + + // 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; +} + +/** + * 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, + entries: USBAdapterFingerprint[], + isWindows: boolean, + conflictProne: boolean, +): [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; + } + + // 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]; + } + } + + if ( + score < USBFingerprintMatchScore.VID_PID_PATH && + entry.pathRegex && + (matchRegex(entry.pathRegex, portInfo.path) || matchRegex(entry.pathRegex, portInfo.pnpId)) + ) { + 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: Adapter, path: string): Promise { + const isWindows = platform() === 'win32'; + const portList = 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, 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); + return true; + } + } + + return false; +} + +export async function findUSBAdapter( + adapter?: Adapter, + 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(); + + logger.debug(() => `Connected devices: ${JSON.stringify(portList)}`, NS); + + for (const portInfo of portList) { + if (path && portInfo.path !== path) { + continue; + } + + 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) { + continue; + } + + const match = matchUSBFingerprint(portInfo, USB_FINGERPRINTS[key as DiscoverableUSBAdapter]!, isWindows, conflictProne); + + // 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]]; + } + } +} + +export async function findmDNSAdapter(path: string): Promise<[adapter: Adapter, 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; + + resolve([adapter, path, baudRate]); + } 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: 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) { + throw new Error(`Cannot discover TCP adapters at this time. Specify valid 'adapter' and 'port' in your configuration.`); + } + + 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: 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) { + try { + const matched = await matchUSBAdapter(adapter, path); + + /* istanbul ignore else */ + if (!matched) { + logger.debug(`Unable to match USB adapter: ${adapter} | ${path}`, NS); + } + } catch (error) { + logger.debug(`Error while trying to match USB adapter (${(error as Error).message}).`, NS); + } + + return [adapter, path]; + } + } + + try { + // default to matching USB + const match = await findUSBAdapter(adapter, path); + + if (!match) { + throw new Error(`No valid USB adapter found`); + } + + // 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/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 24f00a6b53..48013a410c 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..e7402a2460 100644 --- a/src/adapter/tstype.ts +++ b/src/adapter/tstype.ts @@ -1,4 +1,14 @@ -interface NetworkOptions { +export type Adapter = '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 +16,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 +31,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 +48,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 +70,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 3eaf78d6a7..0327a1c3bc 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 30c546181a..cf11aa2684 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 3e36a92e47..d2d4179f59 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..f59057a471 --- /dev/null +++ b/test/adapter/adapter.test.ts @@ -0,0 +1,830 @@ +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, + 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', + 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`, + ); + }); + }); + + 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`}, 'test.db.backup', { + disableLED: false, + }); + }).rejects.toThrow(`Cannot discover TCP adapters at this time. Specify valid 'adapter' and 'port' in your configuration.`); + }); + }); + + describe('USB discovery', () => { + let listSpy: jest.SpyInstance; + let platformSpy: jest.SpyInstance; + + beforeAll(() => { + listSpy = jest.spyOn(SerialPort, 'list'); + listSpy.mockReturnValue([DECONZ_CONBEE_II, EMBER_ZBDONGLE_E, ZSTACK_CC2538, ZBOSS_NORDIC, ZIGATE_PLUSV2]); + + platformSpy = jest.spyOn(os, 'platform'); + platformSpy.mockReturnValue('linux'); + }); + + describe('without config', () => { + it('detects each adapter', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); + + let 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', + }); + + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); + + 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', + }); + + listSpy.mockReturnValueOnce([ZSTACK_CC2538]); + + 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', + }); + + listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); + + 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', + }); + + listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); + + 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 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([ + { + // 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 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]}, {}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(ZBOSSAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: '/dev/ttyUSB0', + adapter: 'zboss', + }); + }); + + it('detects with 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', + }); + + 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 () => { + // 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]}, {}, '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('throws on failure 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('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]}, {}, 'test.db.backup', {disableLED: false}); + }).rejects.toThrow( + `USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`, + ); + }); + }); + + describe('with adapter+path config', () => { + it('detects each adapter', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); + + let 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', + }); + + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); + + adapter = await Adapter.create( + {panID: 0x1a62, channelList: [11]}, + {adapter: 'ember', path: EMBER_ZBDONGLE_E.path}, + 'test.db.backup', + {disableLED: false}, + ); + + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_ZBDONGLE_E.path, + adapter: 'ember', + }); + + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); + + 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 + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_ZBDONGLE_E.path, + adapter: 'ezsp', + }); + + listSpy.mockReturnValueOnce([ZSTACK_CC2538]); + + 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', + }); + + listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); + + 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', + }); + + listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); + + 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 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}, + ); + + 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', 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('detects with 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('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(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]}, + {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}, + ); + + expect(adapter).toBeInstanceOf(ZStackAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: ZSTACK_CC2538.path, + adapter: 'zstack', + }); + }); + + 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`); + }); + }); + + describe('with adapter only config', () => { + it('detects each adapter', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); + + let adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'deconz'}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(DeconzAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: DECONZ_CONBEE_II.path, + adapter: 'deconz', + }); + + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); + + 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', + }); + + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); + + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {adapter: 'ezsp'}, 'test.db.backup', {disableLED: false}); + + expect(adapter).toBeInstanceOf(EZSPAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_ZBDONGLE_E.path, + adapter: 'ezsp', + }); + + listSpy.mockReturnValueOnce([ZSTACK_CC2538]); + + 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', + }); + + listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); + + 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', + }); + + listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); + + 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.`, + ); + }); + }); + + describe('with path only config', () => { + it('detects each adapter', async () => { + listSpy.mockReturnValueOnce([DECONZ_CONBEE_II]); + + let adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {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', + }); + + listSpy.mockReturnValueOnce([EMBER_ZBDONGLE_E]); + + adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {path: EMBER_ZBDONGLE_E.path}, 'test.db.backup', { + disableLED: false, + }); + + expect(adapter).toBeInstanceOf(EmberAdapter); + // @ts-expect-error protected + expect(adapter.serialPortOptions).toStrictEqual({ + path: EMBER_ZBDONGLE_E.path, + adapter: 'ember', + }); + + listSpy.mockReturnValueOnce([ZSTACK_CC2538]); + + 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', + }); + + listSpy.mockReturnValueOnce([ZBOSS_NORDIC]); + + 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', + }); + + listSpy.mockReturnValueOnce([ZIGATE_PLUSV2]); + + 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('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]}, {}, '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.`); + }); + }); +}); 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 016ac5e045..5444f0c9bb 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, @@ -574,7 +548,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}, ); @@ -7327,414 +7301,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'}); @@ -8508,6 +8074,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..aa5ff84b16 --- /dev/null +++ b/test/mockAdapters.ts @@ -0,0 +1,57 @@ +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_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 all 10c4:ea60 +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', // uppercased for extra coverage + manufacturer: 'Texas Instruments', +}; +// 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', + productId: '0100', + manufacturer: 'ZEPHYR', +}; +export const ZIGATE_PLUSV2 = { + path: '/dev/serial/by-id/usb-FTDI_ZiGate_ZIGATE+-if00-port0', + vendorId: '0403', + productId: '6015', +};