diff --git a/bundles/org.openhab.core.config.discovery.usbserial.ser2net/src/main/java/org/openhab/core/config/discovery/usbserial/ser2net/internal/Ser2NetUsbSerialDiscovery.java b/bundles/org.openhab.core.config.discovery.usbserial.ser2net/src/main/java/org/openhab/core/config/discovery/usbserial/ser2net/internal/Ser2NetUsbSerialDiscovery.java
index d898e4e1f7f..da6e87eb38f 100644
--- a/bundles/org.openhab.core.config.discovery.usbserial.ser2net/src/main/java/org/openhab/core/config/discovery/usbserial/ser2net/internal/Ser2NetUsbSerialDiscovery.java
+++ b/bundles/org.openhab.core.config.discovery.usbserial.ser2net/src/main/java/org/openhab/core/config/discovery/usbserial/ser2net/internal/Ser2NetUsbSerialDiscovery.java
@@ -125,9 +125,9 @@ public synchronized void doSingleScan() {
lastScanResult = scanResult;
- removed.stream().forEach(this::announceRemovedDevice);
- added.stream().forEach(this::announceAddedDevice);
- unchanged.stream().forEach(this::announceAddedDevice);
+ removed.forEach(this::announceRemovedDevice);
+ added.forEach(this::announceAddedDevice);
+ unchanged.forEach(this::announceAddedDevice);
logger.debug("Completed ser2net USB-Serial mDNS single discovery scan");
}
diff --git a/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.classpath b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.classpath
new file mode 100644
index 00000000000..58cd399d639
--- /dev/null
+++ b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.classpath
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.project b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.project
new file mode 100644
index 00000000000..ad2f4ec236a
--- /dev/null
+++ b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.config.discovery.usbserial.javaxusb
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/NOTICE b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/NOTICE
new file mode 100644
index 00000000000..6c17d0d8a45
--- /dev/null
+++ b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/NOTICE
@@ -0,0 +1,14 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-core
+
diff --git a/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/pom.xml b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/pom.xml
new file mode 100644
index 00000000000..59a092e7d41
--- /dev/null
+++ b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/pom.xml
@@ -0,0 +1,30 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bundles
+ org.openhab.core.reactor.bundles
+ 4.2.0-SNAPSHOT
+
+
+ org.openhab.core.config.discovery.usbserial.windowsregistry
+
+ openHAB Core :: Bundles :: Configuration USB-Serial Discovery for Windows
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.discovery.usbserial
+ ${project.version}
+
+
+ net.java.dev.jna
+ jna-platform
+ 5.13.0
+
+
+
+
diff --git a/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/src/main/java/org/openhab/core/config/discovery/usbserial/windowsregistry/internal/WindowsUsbSerialDiscovery.java b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/src/main/java/org/openhab/core/config/discovery/usbserial/windowsregistry/internal/WindowsUsbSerialDiscovery.java
new file mode 100644
index 00000000000..5c19cd089cc
--- /dev/null
+++ b/bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/src/main/java/org/openhab/core/config/discovery/usbserial/windowsregistry/internal/WindowsUsbSerialDiscovery.java
@@ -0,0 +1,261 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.config.discovery.usbserial.windowsregistry.internal;
+
+import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE;
+
+import java.time.Duration;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.common.ThreadFactoryBuilder;
+import org.openhab.core.config.discovery.usbserial.UsbSerialDeviceInformation;
+import org.openhab.core.config.discovery.usbserial.UsbSerialDiscovery;
+import org.openhab.core.config.discovery.usbserial.UsbSerialDiscoveryListener;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sun.jna.Platform;
+import com.sun.jna.platform.win32.Advapi32Util;
+
+/**
+ * This is a {@link UsbSerialDiscovery} implementation component for Windows.
+ * It parses the Windows registry for USB device entries.
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = UsbSerialDiscovery.class, name = WindowsUsbSerialDiscovery.SERVICE_NAME)
+public class WindowsUsbSerialDiscovery implements UsbSerialDiscovery {
+
+ protected static final String SERVICE_NAME = "usb-serial-discovery-windows";
+
+ // registry accessor strings
+ private static final String USB_REGISTRY_ROOT = "SYSTEM\\CurrentControlSet\\Enum\\USB";
+ private static final String BACKSLASH = "\\";
+ private static final String PREFIX_PID = "PID_";
+ private static final String PREFIX_VID = "VID_";
+ private static final String PREFIX_HEX = "0x";
+ private static final String SPLIT_IDS = "&";
+ private static final String SPLIT_VALUES = ";";
+ private static final String KEY_MANUFACTURER = "Mfg";
+ private static final String KEY_PRODUCT = "DeviceDesc";
+ private static final String KEY_DEVICE_PARAMETERS = "Device Parameters";
+ private static final String KEY_SERIAL_PORT = "PortName";
+
+ private final Logger logger = LoggerFactory.getLogger(WindowsUsbSerialDiscovery.class);
+ private final Set discoveryListeners = new CopyOnWriteArraySet<>();
+ private final Duration scanInterval = Duration.ofSeconds(15);
+ private final ScheduledExecutorService scheduler;
+
+ private Set lastScanResult = new HashSet<>();
+ private @Nullable ScheduledFuture> scanTask;
+
+ @Activate
+ public WindowsUsbSerialDiscovery() {
+ scheduler = Executors.newSingleThreadScheduledExecutor(
+ ThreadFactoryBuilder.create().withName(SERVICE_NAME).withDaemonThreads(true).build());
+ }
+
+ private void announceAddedDevice(UsbSerialDeviceInformation deviceInfo) {
+ for (UsbSerialDiscoveryListener listener : discoveryListeners) {
+ listener.usbSerialDeviceDiscovered(deviceInfo);
+ }
+ }
+
+ private void announceRemovedDevice(UsbSerialDeviceInformation deviceInfo) {
+ for (UsbSerialDiscoveryListener listener : discoveryListeners) {
+ listener.usbSerialDeviceRemoved(deviceInfo);
+ }
+ }
+
+ @Deactivate
+ public void deactivate() {
+ stopBackgroundScanning();
+ lastScanResult.clear();
+ }
+
+ @Override
+ public synchronized void doSingleScan() {
+ Set scanResult = scanAllUsbDevicesInformation();
+ Set added = setDifference(scanResult, lastScanResult);
+ Set removed = setDifference(lastScanResult, scanResult);
+ Set unchanged = setDifference(scanResult, added);
+
+ lastScanResult = scanResult;
+
+ removed.forEach(this::announceRemovedDevice);
+ added.forEach(this::announceAddedDevice);
+ unchanged.forEach(this::announceAddedDevice);
+ }
+
+ private Set setDifference(Set set1, Set set2) {
+ Set result = new HashSet<>(set1);
+ result.removeAll(set2);
+ return result;
+ }
+
+ @Override
+ public void registerDiscoveryListener(UsbSerialDiscoveryListener listener) {
+ discoveryListeners.add(listener);
+ for (UsbSerialDeviceInformation deviceInfo : lastScanResult) {
+ listener.usbSerialDeviceDiscovered(deviceInfo);
+ }
+ }
+
+ @Override
+ public void unregisterDiscoveryListener(UsbSerialDiscoveryListener listener) {
+ discoveryListeners.remove(listener);
+ }
+
+ /**
+ * Traverse the USB tree in Windows registry and return a set of USB device information.
+ *
+ * @return a set of USB device information.
+ */
+ public Set scanAllUsbDevicesInformation() {
+ if (!Platform.isWindows()) {
+ return Set.of();
+ }
+
+ Set result = new HashSet<>();
+ String[] deviceKeys = Advapi32Util.registryGetKeys(HKEY_LOCAL_MACHINE, USB_REGISTRY_ROOT);
+
+ for (String deviceKey : deviceKeys) {
+ logger.trace("{}", deviceKey);
+
+ if (!deviceKey.startsWith(PREFIX_VID)) {
+ continue;
+ }
+
+ String[] ids = deviceKey.split(SPLIT_IDS);
+ if (ids.length < 2) {
+ continue;
+ }
+
+ if (!ids[1].startsWith(PREFIX_PID)) {
+ continue;
+ }
+
+ int vendorId;
+ int productId;
+ try {
+ vendorId = Integer.decode(PREFIX_HEX + ids[0].substring(4));
+ productId = Integer.decode(PREFIX_HEX + ids[1].substring(4));
+ } catch (NumberFormatException e) {
+ continue;
+ }
+
+ String serialNumber = ids.length > 2 ? ids[2] : null;
+
+ String devicePath = USB_REGISTRY_ROOT + BACKSLASH + deviceKey;
+ String[] interfaceNames = Advapi32Util.registryGetKeys(HKEY_LOCAL_MACHINE, devicePath);
+
+ int interfaceId = 0;
+ for (String interfaceName : interfaceNames) {
+ logger.trace(" interfaceId:{}, interfaceName:{}", interfaceId, interfaceName);
+
+ String interfacePath = devicePath + BACKSLASH + interfaceName;
+ TreeMap values = Advapi32Util.registryGetValues(HKEY_LOCAL_MACHINE, interfacePath);
+
+ if (logger.isTraceEnabled()) {
+ for (Entry value : values.entrySet()) {
+ logger.trace(" {}={}", value.getKey(), value.getValue());
+ }
+ }
+
+ String manufacturer;
+ Object manufacturerValue = values.get(KEY_MANUFACTURER);
+ if (manufacturerValue instanceof String manufacturerString) {
+ String[] manufacturerData = manufacturerString.split(SPLIT_VALUES);
+ if (manufacturerData.length < 2) {
+ continue;
+ }
+ manufacturer = manufacturerData[1];
+ } else {
+ continue;
+ }
+
+ String product;
+ Object productValue = values.get(KEY_PRODUCT);
+ if (productValue instanceof String productString) {
+ String[] productData = productString.split(SPLIT_VALUES);
+ if (productData.length < 2) {
+ continue;
+ }
+ product = productData[1];
+ } else {
+ continue;
+ }
+
+ String serialPort = "";
+ String[] interfaceSubKeys = Advapi32Util.registryGetKeys(HKEY_LOCAL_MACHINE, interfacePath);
+
+ for (String interfaceSubKey : interfaceSubKeys) {
+ if (!KEY_DEVICE_PARAMETERS.equals(interfaceSubKey)) {
+ continue;
+ }
+ String deviceParametersPath = interfacePath + BACKSLASH + interfaceSubKey;
+ TreeMap deviceParameterValues = Advapi32Util.registryGetValues(HKEY_LOCAL_MACHINE,
+ deviceParametersPath);
+ Object serialPortValue = deviceParameterValues.get(KEY_SERIAL_PORT);
+ if (serialPortValue instanceof String serialPortString) {
+ serialPort = serialPortString;
+ }
+ break;
+ }
+
+ UsbSerialDeviceInformation usbSerialDeviceInformation = new UsbSerialDeviceInformation(vendorId,
+ productId, serialNumber, manufacturer, product, interfaceId, interfaceName, serialPort);
+
+ logger.debug("Add {}", usbSerialDeviceInformation);
+ result.add(usbSerialDeviceInformation);
+
+ interfaceId++;
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public synchronized void startBackgroundScanning() {
+ if (Platform.isWindows()) {
+ ScheduledFuture> scanTask = this.scanTask;
+ if (scanTask == null || scanTask.isDone()) {
+ this.scanTask = scheduler.scheduleWithFixedDelay(this::doSingleScan, 0, scanInterval.toSeconds(),
+ TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ @Override
+ public synchronized void stopBackgroundScanning() {
+ ScheduledFuture> scanTask = this.scanTask;
+ if (scanTask != null) {
+ scanTask.cancel(false);
+ }
+ this.scanTask = null;
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 5035de7e1a7..8f6fd77f721 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -40,6 +40,7 @@
org.openhab.core.config.discovery.usbserial
org.openhab.core.config.discovery.usbserial.linuxsysfs
org.openhab.core.config.discovery.usbserial.ser2net
+ org.openhab.core.config.discovery.usbserial.windowsregistry
org.openhab.core.config.discovery.upnp
org.openhab.core.config.dispatch
org.openhab.core.config.serial
diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml
index 6f25f1e80c2..94d766da68a 100644
--- a/features/karaf/openhab-core/src/main/feature/feature.xml
+++ b/features/karaf/openhab-core/src/main/feature/feature.xml
@@ -519,8 +519,19 @@
mvn:org.openhab.core.bundles/org.openhab.core.config.serial/${project.version}
mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial/${project.version}
- mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial.linuxsysfs/${project.version}
+
+
+ req:osgi.native;filter:="(osgi.native.osname=Linux)"
+ mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial.linuxsysfs/${project.version}
+
+
mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial.ser2net/${project.version}
+
+
+ req:osgi.native;filter:="(osgi.native.osname=Windows*)"
+ mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial.windowsregistry/${project.version}
+
+
mvn:org.openhab.core.bundles/org.openhab.core.io.transport.serial/${project.version}
mvn:org.openhab.core.bundles/org.openhab.core.io.transport.serial.rxtx/${project.version}
mvn:org.openhab.core.bundles/org.openhab.core.io.transport.serial.rxtx.rfc2217/${project.version}