Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions src/adapter/adapterDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const USB_FINGERPRINTS: Record<DiscoverableUsbAdapter, UsbAdapterFingerprint[]>
manufacturer: "Nabu Casa",
// /dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_3abe54797c91ed118fc3cad13b20a111-if00-port0
pathRegex: ".*Nabu_Casa_SkyConnect.*",
options: {rtscts: true},
},
{
// Home Assistant Connect ZBT-2
Expand All @@ -98,13 +99,15 @@ const USB_FINGERPRINTS: Record<DiscoverableUsbAdapter, UsbAdapterFingerprint[]>
// /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
options: {rtscts: true},
},
{
// SMLight slzb-07mg24
vendorId: "10c4",
productId: "ea60",
manufacturer: "SMLIGHT",
pathRegex: ".*slzb-07mg24.*",
options: {rtscts: true},
},
{
// Sonoff ZBDongle-E V2 (CH variant)
Expand Down Expand Up @@ -470,27 +473,35 @@ export async function findUsbAdapter(adapter?: Adapter, path?: string): Promise<

logger.debug(() => `Connected devices: ${JSON.stringify(portList)}`, NS);

let bestMatch: ReturnType<typeof findUsbAdapterBestMatch> | undefined;

for (const portInfo of portList) {
if (path && portInfo.path !== path) {
continue;
}

const conflictProne = USB_FINGERPRINTS_CONFLICT_IDS.includes(`${portInfo.vendorId}:${portInfo.productId}`);
const bestMatch = findUsbAdapterBestMatch(adapter, portInfo, isWindows, conflictProne);

if (bestMatch) {
logger.info(
() => `Matched adapter: ${JSON.stringify(portInfo)} => ${bestMatch[0]}: path=${bestMatch[1][0]}, score=${bestMatch[1][1]}`,
NS,
);
return {
adapter: bestMatch[0],
path: bestMatch[1][0],
rtscts: bestMatch[1][2].options?.rtscts,
baudRate: bestMatch[1][2].options?.baudRate,
};
const match = findUsbAdapterBestMatch(adapter, portInfo, isWindows, conflictProne);

if (match && (!bestMatch || bestMatch[1][1] < match[1][1])) {
bestMatch = match;

if (match[1][1] === UsbFingerprintMatchScore.VidPidManufPath) {
// got best possible match, exit loop
break;
}
}
}

if (bestMatch) {
logger.info(() => `Matched adapter=${bestMatch[0]} path=${bestMatch[1][0]}, score=${bestMatch[1][1]}`, NS);
return {
adapter: bestMatch[0],
path: bestMatch[1][0],
rtscts: bestMatch[1][2].options?.rtscts,
baudRate: bestMatch[1][2].options?.baudRate,
};
}
}

function getMdnsRadioAdapter(radio: string): Adapter {
Expand Down
35 changes: 35 additions & 0 deletions test/adapter/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import {
EMBER_ZBDONGLE_E,
EMBER_ZBDONGLE_E_CP,
ZBOSS_NORDIC,
ZBT_1_PNPID,
ZBT_2,
ZIGATE_PLUSV2,
ZSTACK_CC2538,
ZSTACK_SMLIGHT_SLZB_06P10,
ZSTACK_SMLIGHT_SLZB_07,
ZSTACK_ZBDONGLE_P,
ZWA_2_CONFLICT,
} from "../mockAdapters";

const mockPlatform = vi.fn(() => "linux");
Expand Down Expand Up @@ -544,6 +546,7 @@ describe("Adapter", () => {
expect(adapter.serialPortOptions).toStrictEqual({
path: EMBER_SKYCONNECT.path,
adapter: "ember",
rtscts: true,
});

listSpy.mockReturnValueOnce([{...ZSTACK_ZBDONGLE_P, path: "/dev/ttyACM0"}]);
Expand Down Expand Up @@ -607,6 +610,7 @@ describe("Adapter", () => {
expect(adapter.serialPortOptions).toStrictEqual({
path: ZSTACK_SMLIGHT_SLZB_07.path,
adapter: "ember",
rtscts: true,
});
});

Expand Down Expand Up @@ -641,6 +645,36 @@ describe("Adapter", () => {
`USB adapter discovery error (No valid USB adapter found). Specify valid 'adapter' and 'port' in your configuration.`,
);
});

it("detects SkyConnect ZBT-1 when ZWA-2 present", async () => {
listSpy.mockReturnValueOnce([ZWA_2_CONFLICT, ZBT_1_PNPID]);

const adapter = await Adapter.create({panID: 0x1a62, channelList: [11]}, {baudRate: 115200}, "test.db.backup", {disableLED: false});

expect(adapter).toBeInstanceOf(EmberAdapter);
// @ts-expect-error protected
expect(adapter.serialPortOptions).toStrictEqual({
path: ZBT_1_PNPID.path,
adapter: "ember",
baudRate: 115200,
rtscts: true,
});
});

it("detects SkyConnect ZBT-2 when ZWA-2 present", async () => {
listSpy.mockReturnValueOnce([ZWA_2_CONFLICT, ZBT_2]);

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: ZBT_2.path,
adapter: "ember",
baudRate: 460800,
rtscts: true,
});
});
});

describe("with adapter+path config", () => {
Expand Down Expand Up @@ -795,6 +829,7 @@ describe("Adapter", () => {
expect(adapter.serialPortOptions).toStrictEqual({
path: EMBER_SKYCONNECT.path,
adapter: "ember",
rtscts: true,
});

listSpy.mockReturnValueOnce([{...ZSTACK_ZBDONGLE_P, path: "/dev/ttyACM0"}]);
Expand Down
14 changes: 14 additions & 0 deletions test/mockAdapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,17 @@ export const ZBT_2 = {
productId: "4001",
manufacturer: "Nabu Casa",
};
export const ZBT_1_PNPID = {
path: "/dev/ttyUSB0",
pnpId: "usb-Nabu_Casa_SkyConnect_v1.0_92390c41b6d8ed11a3436b6142c613ac-if00-port0",
vendorId: "10c4",
productId: "ea60",
manufacturer: "Nabu Casa",
};
export const ZWA_2_CONFLICT = {
path: "/dev/ttyACM0",
manufacturer: "Nabu Casa",
pnpId: "usb-Nabu_Casa_ZWA-2_81B53EF0C8EC-if00",
vendorId: "303a",
productId: "4001",
};