deviceInfos) {
+ for (UsbSerialDeviceInformation deviceInfo : deviceInfos) {
+ for (UsbSerialDiscoveryListener listener : discoveryListeners) {
+ listener.usbSerialDeviceRemoved(deviceInfo);
+ }
+ }
+ }
+
+}
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/SysfsUsbSerialScanner.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/SysfsUsbSerialScanner.java
new file mode 100644
index 00000000000..476698b4936
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/SysfsUsbSerialScanner.java
@@ -0,0 +1,274 @@
+/**
+ * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.internal;
+
+import static java.nio.file.Files.*;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDeviceInformation;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link UsbSerialScanner} that scans the system for USB devices which provide a serial port by inspecting the
+ * so-called 'sysfs' (see also https://en.wikipedia.org/wiki/Sysfs) provided by Linux (a pseudo file system provided by
+ * the Linux kernel usually mounted at '/sys').
+ *
+ * A scan starts by inspecting the contents of the directory '/sys/class/tty'. This directory contains a symbolic link
+ * for every serial port style device that points to the device information provided by the sysfs in some subdirectory
+ * of '/sys/devices'.
+ *
+ * The scan considers only those serial ports for which the corresponding device file (in folder '/dev'; e.g.:
+ * '/dev/ttyUSB0') is both readable and writable, as otherwise the serial port cannot be used by any binding. For those
+ * serial ports, the scan checks whether the serial port actually originates from a USB device, by inspecting the
+ * information provided by the sysfs in the folder pointed to by the symbolic link.
+ *
+ * If the device providing the serial port is a USB device, information about the device (vendor ID, product ID, etc.)
+ * is collected from the sysfs and returned together with the name of the serial port in form of a
+ * {@link UsbSerialDeviceInformation}.
+ *
+ * @author Henning Sudbrock - initial contribution
+ */
+@Component(configurationPid = "discovery.usbserial.linuxsysfs.usbserialscanner")
+@NonNullByDefault
+public class SysfsUsbSerialScanner implements UsbSerialScanner {
+
+ private final Logger logger = LoggerFactory.getLogger(SysfsUsbSerialScanner.class);
+
+ public static final String SYSFS_TTY_DEVICES_DIRECTORY_ATTRIBUTE = "sysfsTtyDevicesPath";
+ public static final String DEV_DIRECTORY_ATTRIBUTE = "devPath";
+
+ private static final String SYSFS_TTY_DEVICES_DIRECTORY_DEFAULT = "/sys/class/tty";
+ private static final String DEV_DIRECTORY_DEFAULT = "/dev";
+
+ private String sysfsTtyDevicesDirectory = SYSFS_TTY_DEVICES_DIRECTORY_DEFAULT;
+ private String devDirectory = DEV_DIRECTORY_DEFAULT;
+
+ private static final String SYSFS_FILENAME_USB_VENDOR_ID = "idVendor";
+ private static final String SYSFS_FILENAME_USB_PRODUCT_ID = "idProduct";
+ private static final String SYSFS_FILENAME_USB_SERIAL_NUMBER = "serial";
+ private static final String SYSFS_FILENAME_USB_MANUFACTURER = "manufacturer";
+ private static final String SYSFS_FILENAME_USB_PRODUCT = "product";
+ private static final String SYSFS_FILENAME_USB_INTERFACE_NUMBER = "bInterfaceNumber";
+ private static final String SYSFS_FILENAME_USB_INTERFACE = "interface";
+
+ /**
+ * In the sysfs, directories for USB interfaces have the following format (cf., e.g.,
+ * http://www.linux-usb.org/FAQ.html#i6), where there can be one or more USB port numbers separated by dots.
+ *
+ *
+ * {@code <#bus>-<#port>.<#port>:<#config>:<#interface>}
+ *
+ *
+ * Example: {@code 3-1.3:1.0}
+ *
+ * This format is captured by this {@link Pattern}.
+ */
+ private static final Pattern SYSFS_USB_INTERFACE_DIRECTORY_PATTERN = Pattern
+ .compile("\\d+-(\\d+\\.?)*\\d+:\\d+\\.\\d+");
+
+ @Activate
+ protected void activate(Map config) {
+ extractConfiguration(config);
+ }
+
+ @Modified
+ protected void modified(Map config) {
+ extractConfiguration(config);
+ }
+
+ @Override
+ public Set scan() throws IOException {
+ Set result = new HashSet<>();
+
+ for (SerialPortInfo serialPortInfo : getSerialPortInfos()) {
+ try {
+ UsbSerialDeviceInformation usbSerialDeviceInfo = tryGetUsbSerialDeviceInformation(serialPortInfo);
+ if (usbSerialDeviceInfo != null) {
+ result.add(usbSerialDeviceInfo);
+ }
+ } catch (IOException e) {
+ logger.warn("Could not extract USB device information for serial port {}: {}", serialPortInfo,
+ e.getMessage());
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Gets the set of all found serial ports, by searching through the tty devices directory in the sysfs and
+ * checking for each found serial port if the device file in the devices folder is both readable and writable.
+ *
+ * @throws IOException If there is a problem reading files from the sysfs tty devices directory.
+ */
+ private Set getSerialPortInfos() throws IOException {
+ Set result = new HashSet<>();
+
+ try (DirectoryStream sysfsTtyPaths = newDirectoryStream(Paths.get(sysfsTtyDevicesDirectory))) {
+ for (Path sysfsTtyPath : sysfsTtyPaths) {
+ String serialPortName = sysfsTtyPath.getFileName().toString();
+ Path devicePath = Paths.get(devDirectory).resolve(serialPortName);
+ Path sysfsDevicePath = getSysfsDevicePath(sysfsTtyPath);
+ if (sysfsDevicePath != null && isAccessible(devicePath)) {
+ result.add(new SerialPortInfo(devicePath, sysfsDevicePath));
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private boolean isAccessible(Path devicePath) {
+ return exists(devicePath) && isWritable(devicePath) && isReadable(devicePath);
+ }
+
+ /**
+ * In the sysfs, the directory 'class/tty' contains a symbolic link for every serial port style device, i.e., also
+ * for serial devices. This symbolic link points to the directory for that device within the sysfs device tree. This
+ * method returns the directory to which this symbolic link points for a given serial port.
+ *
+ * If the symbolic link cannot be converted to the real path, null is returned and a warning is logged.
+ */
+ @Nullable
+ private Path getSysfsDevicePath(Path ttyFile) {
+ try {
+ return ttyFile.toRealPath();
+ } catch (IOException e) {
+ logger.warn("Could not find the device path for {} in the sysfs: {}", ttyFile, e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Checks whether the provided device path in sysfs points to a folder within the sysfs description of a USB device;
+ * if so, extracts the USB device information from sysfs and constructs a {@link UsbSerialDeviceInformation} using
+ * the {@link SerialPortInfo} and the information about the USB device gathered from sysfs.
+ *
+ * Returns null if the path does not point to a folder within the sysfs description of a USB device.
+ */
+ @Nullable
+ private UsbSerialDeviceInformation tryGetUsbSerialDeviceInformation(SerialPortInfo serialPortInfo)
+ throws IOException {
+ Path usbInterfacePath = getUsbInterfaceParentPath(serialPortInfo.getSysfsPath());
+
+ if (usbInterfacePath == null) {
+ return null;
+ }
+
+ Path usbDevicePath = usbInterfacePath.getParent();
+ if (isUsbDevicePath(usbDevicePath)) {
+ return createUsbSerialDeviceInformation(usbDevicePath, usbInterfacePath,
+ serialPortInfo.getDevicePath().toString());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Walks up the directory structure of a path in the sysfs, trying to find a directory that represents an interface
+ * of a USB device.
+ */
+ @Nullable
+ private Path getUsbInterfaceParentPath(Path sysfsPath) {
+ if (sysfsPath.getFileName() == null) {
+ return null;
+ } else if (SYSFS_USB_INTERFACE_DIRECTORY_PATTERN.matcher(sysfsPath.getFileName().toString()).matches()) {
+ return sysfsPath;
+ } else {
+ Path parentPath = sysfsPath.getParent();
+ if (parentPath == null) {
+ return null;
+ } else {
+ return getUsbInterfaceParentPath(parentPath);
+ }
+ }
+ }
+
+ private boolean isUsbDevicePath(Path path) {
+ return containsFile(path, SYSFS_FILENAME_USB_PRODUCT_ID) && containsFile(path, SYSFS_FILENAME_USB_VENDOR_ID);
+ }
+
+ /**
+ * Constructs a {@link UsbSerialDeviceInformation} from a serial port and the information found in the sysfs about a
+ * USB device.
+ */
+ private UsbSerialDeviceInformation createUsbSerialDeviceInformation(Path usbDevicePath, Path usbInterfacePath,
+ String serialPortName) throws IOException {
+ int vendorId = Integer.parseInt(getContent(usbDevicePath.resolve(SYSFS_FILENAME_USB_VENDOR_ID)), 16);
+ int productId = Integer.parseInt(getContent(usbDevicePath.resolve(SYSFS_FILENAME_USB_PRODUCT_ID)), 16);
+
+ String serialNumber = getContentIfFileExists(usbDevicePath.resolve(SYSFS_FILENAME_USB_SERIAL_NUMBER));
+ String manufacturer = getContentIfFileExists(usbDevicePath.resolve(SYSFS_FILENAME_USB_MANUFACTURER));
+ String product = getContentIfFileExists(usbDevicePath.resolve(SYSFS_FILENAME_USB_PRODUCT));
+
+ int interfaceNumber = Integer.parseInt(getContent(usbInterfacePath.resolve(SYSFS_FILENAME_USB_INTERFACE_NUMBER)),
+ 16);
+ String interfaceDescription = getContentIfFileExists(usbInterfacePath.resolve(SYSFS_FILENAME_USB_INTERFACE));
+
+ return new UsbSerialDeviceInformation(vendorId, productId, serialNumber, manufacturer, product, interfaceNumber,
+ interfaceDescription, serialPortName);
+ }
+
+ private boolean containsFile(Path directoryPath, String filename) {
+ Path filePath = directoryPath.resolve(filename);
+ return exists(filePath) && !isDirectory(filePath);
+ }
+
+ private String getContent(Path path) throws IOException {
+ return new String(readAllBytes(path)).trim();
+ }
+
+ @Nullable
+ private String getContentIfFileExists(Path path) throws IOException {
+ return exists(path) ? getContent(path) : null;
+ }
+
+ private void extractConfiguration(Map config) {
+ sysfsTtyDevicesDirectory = config
+ .getOrDefault(SYSFS_TTY_DEVICES_DIRECTORY_ATTRIBUTE, SYSFS_TTY_DEVICES_DIRECTORY_DEFAULT).toString();
+ devDirectory = config.getOrDefault(DEV_DIRECTORY_ATTRIBUTE, DEV_DIRECTORY_DEFAULT).toString();
+ }
+
+ private static class SerialPortInfo {
+ private final Path devicePath;
+ private final Path sysfsPath;
+
+ public SerialPortInfo(Path devicePath, Path sysfsPath) {
+ this.devicePath = devicePath;
+ this.sysfsPath = sysfsPath;
+ }
+
+ public Path getDevicePath() {
+ return devicePath;
+ }
+
+ public Path getSysfsPath() {
+ return sysfsPath;
+ }
+ }
+
+}
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/UsbSerialScanner.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/UsbSerialScanner.java
new file mode 100644
index 00000000000..2197232aa97
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/UsbSerialScanner.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.internal;
+
+import java.io.IOException;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDeviceInformation;
+
+/**
+ * Implementations of this interface scan for serial ports provided by USB devices.
+ *
+ * @author Henning Sudbrock - initial contribution
+ */
+@NonNullByDefault
+public interface UsbSerialScanner {
+
+ /**
+ * Performs a single scan for serial ports provided by USB devices.
+ *
+ * @return A collection containing all scan results.
+ * @throws IOException if an I/O issue prevented the scan. Note that implementors are free to swallow I/O issues
+ * that occur when trying to read the information about a single USB device or serial port, so that
+ * information about other devices can still be retrieved. (Such issues should nevertheless be logged by
+ * implementors.)
+ */
+ Set scan() throws IOException;
+
+}
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/.classpath b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/.classpath
new file mode 100644
index 00000000000..3827701922c
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/.classpath
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/.project b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/.project
new file mode 100644
index 00000000000..d46054068a3
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/.project
@@ -0,0 +1,28 @@
+
+
+ org.eclipse.smarthome.config.discovery.usbserial.test
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.pde.ManifestBuilder
+
+
+
+
+ org.eclipse.pde.SchemaBuilder
+
+
+
+
+
+ org.eclipse.pde.PluginNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/.settings/org.eclipse.jdt.core.prefs b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..6e80039d3b8
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,8 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/META-INF/MANIFEST.MF b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/META-INF/MANIFEST.MF
new file mode 100644
index 00000000000..f488f9b06e9
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/META-INF/MANIFEST.MF
@@ -0,0 +1,25 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Eclipse SmartHome Configuration Discovery USB-Serial Tests
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Bundle-SymbolicName: org.eclipse.smarthome.config.discovery.usbserial.test;singleton:=true
+Bundle-Vendor: Eclipse.org/SmartHome
+Bundle-Version: 0.10.0.qualifier
+Fragment-Host: org.eclipse.smarthome.config.discovery.usbserial
+Import-Package:
+ org.eclipse.jdt.annotation;resolution:=optional,
+ org.eclipse.smarthome.config.discovery.usbserial.testutil,
+ org.eclipse.smarthome.test,
+ org.eclipse.smarthome.test.java,
+ org.hamcrest;core=split,
+ org.hamcrest.collection,
+ org.hamcrest.core,
+ org.junit;version="4.0.0",
+ org.junit.rules,
+ org.mockito,
+ org.mockito.invocation,
+ org.mockito.stubbing,
+ org.mockito.verification,
+ org.osgi.service.cm
+Export-Package:
+ org.eclipse.smarthome.config.discovery.usbserial.testutil
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/NOTICE b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/NOTICE
new file mode 100644
index 00000000000..b8675cd02e8
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/NOTICE
@@ -0,0 +1,19 @@
+This content is produced and maintained by the Eclipse SmartHome project.
+
+* Project home: https://eclipse.org/smarthome/
+
+== 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/eclipse/smarthome
+
+== Copyright Holders
+
+See the NOTICE file distributed with the source code at
+https://github.com/eclipse/smarthome/blob/master/NOTICE
+for detailed information regarding copyright ownership.
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/build.properties b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/build.properties
new file mode 100644
index 00000000000..0c65482c5cc
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/build.properties
@@ -0,0 +1,5 @@
+source.. = src/test/java/
+output.. = target/test-classes/
+bin.includes = META-INF/,\
+ .,\
+ NOTICE
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/org.eclipse.smarthome.config.discovery.usbserial.test.launch b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/org.eclipse.smarthome.config.discovery.usbserial.test.launch
new file mode 100644
index 00000000000..685619dcef8
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/org.eclipse.smarthome.config.discovery.usbserial.test.launch
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/pom.xml b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/pom.xml
new file mode 100644
index 00000000000..a8052c7f4e6
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/pom.xml
@@ -0,0 +1,108 @@
+
+
+ 4.0.0
+
+
+ config
+ org.eclipse.smarthome.bundles
+ 0.10.0-SNAPSHOT
+
+ org.eclipse.smarthome.config
+ org.eclipse.smarthome.config.discovery.usbserial.test
+ eclipse-test-plugin
+ Eclipse SmartHome Configuration Discovery UsbSerial Tests
+
+
+ org.eclipse.smarthome.config.discovery.usbserial.test
+ org.eclipse.smarthome.config.discovery.usbserial.test
+
+
+
+
+
+ ${tycho-groupid}
+ target-platform-configuration
+
+
+
+
+
+ eclipse-plugin
+ org.eclipse.equinox.ds
+ 0.0.0
+
+
+ eclipse-plugin
+ org.eclipse.equinox.cm
+ 0.0.0
+
+
+ eclipse-plugin
+ org.eclipse.equinox.event
+ 0.0.0
+
+
+ eclipse-plugin
+ org.eclipse.smarthome.core
+ 0.0.0
+
+
+ eclipse-plugin
+ org.eclipse.smarthome.core.thing
+ 0.0.0
+
+
+ eclipse-plugin
+ org.eclipse.smarthome.core.thing.xml
+ 0.0.0
+
+
+
+
+
+
+ ${tycho-groupid}
+ tycho-surefire-plugin
+
+
+
+ org.eclipse.equinox.ds
+ 1
+ true
+
+
+ org.eclipse.equinox.cm
+ 4
+ true
+
+
+ org.eclipse.equinox.event
+ 4
+ true
+
+
+ org.eclipse.smarthome.core
+ 4
+ true
+
+
+ org.eclipse.smarthome.core.thing
+ 4
+ true
+
+
+ org.eclipse.smarthome.core.thing.xml
+ 4
+ true
+
+
+ org.eclipse.smarthome.config.core
+ 4
+ true
+
+
+
+
+
+
+
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/internal/UsbSerialDiscoveryServiceTest.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/internal/UsbSerialDiscoveryServiceTest.java
new file mode 100644
index 00000000000..235472f35bc
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/internal/UsbSerialDiscoveryServiceTest.java
@@ -0,0 +1,200 @@
+package org.eclipse.smarthome.config.discovery.usbserial.internal;
+
+import static java.util.Arrays.asList;
+import static org.eclipse.smarthome.config.discovery.DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.collection.IsEmptyCollection.empty;
+import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Hashtable;
+
+import org.eclipse.smarthome.config.discovery.DiscoveryListener;
+import org.eclipse.smarthome.config.discovery.DiscoveryResult;
+import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDeviceInformation;
+import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDiscovery;
+import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDiscoveryParticipant;
+import org.eclipse.smarthome.config.discovery.usbserial.testutil.UsbSerialDeviceInformationGenerator;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.eclipse.smarthome.test.java.JavaOSGiTest;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+/**
+ * Unit tests for the {@link UsbSerialDiscoveryService}.
+ *
+ * @author Henning Sudbrock - initial contribution
+ */
+public class UsbSerialDiscoveryServiceTest extends JavaOSGiTest {
+
+ private UsbSerialDiscovery usbSerialDiscovery;
+ private UsbSerialDiscoveryService usbSerialDiscoveryService;
+
+ private final UsbSerialDeviceInformationGenerator usbSerialDeviceInformationGenerator = new UsbSerialDeviceInformationGenerator();
+
+ @Before
+ public void setup() {
+ usbSerialDiscovery = mock(UsbSerialDiscovery.class);
+ registerService(usbSerialDiscovery);
+
+ usbSerialDiscoveryService = getService(UsbSerialDiscoveryService.class);
+ }
+
+ @Test
+ public void testSettingUsbSerialDiscoveryStartsBackgroundDiscoveryIfEnabled()
+ throws InterruptedException, IOException {
+ // Background discovery is enabled by default, hence no need to set it explicitly again. In consequence,
+ // background discovery is started >=1 times (both in the activator and in startBackgroundDiscovery).
+ verify(usbSerialDiscovery, atLeast(1)).startBackgroundScanning();
+ }
+
+ @Test
+ public void testSettingUsbSerialDiscoveryDoesNotStartBackgroundDiscoveryIfDisabled()
+ throws IOException, InterruptedException {
+ setBackgroundDiscovery(false);
+ unregisterService(usbSerialDiscovery);
+
+ UsbSerialDiscovery anotherUsbSerialDiscovery = mock(UsbSerialDiscovery.class);
+ registerService(anotherUsbSerialDiscovery);
+ verify(anotherUsbSerialDiscovery, never()).startBackgroundScanning();
+ }
+
+ @Test
+ public void testRegistersAsUsbserialDiscoveryListener() {
+ verify(usbSerialDiscovery, times(1)).registerDiscoveryListener(usbSerialDiscoveryService);
+ }
+
+ @Test
+ public void testUnregistersAsUsbserialDiscoveryListener() {
+ unregisterService(usbSerialDiscovery);
+ verify(usbSerialDiscovery, times(1)).unregisterDiscoveryListener(usbSerialDiscoveryService);
+ }
+
+ @Test
+ public void testSupportedThingTypesAreRetrievedFromDiscoveryParticipants() {
+ // with no discovery participants available, no thing types are supported.
+ assertThat(usbSerialDiscoveryService.getSupportedThingTypes(), is(empty()));
+
+ // with two discovery participants available, the thing types supported by them are supported.
+ ThingTypeUID thingTypeA = new ThingTypeUID("a:b:c");
+ ThingTypeUID thingTypeB = new ThingTypeUID("d:e:f");
+ ThingTypeUID thingTypeC = new ThingTypeUID("g:h:i");
+
+ UsbSerialDiscoveryParticipant discoveryParticipantA = mock(UsbSerialDiscoveryParticipant.class);
+ when(discoveryParticipantA.getSupportedThingTypeUIDs())
+ .thenReturn(new HashSet<>(asList(thingTypeA, thingTypeB)));
+ registerService(discoveryParticipantA);
+
+ UsbSerialDiscoveryParticipant discoveryParticipantB = mock(UsbSerialDiscoveryParticipant.class);
+ when(discoveryParticipantB.getSupportedThingTypeUIDs())
+ .thenReturn(new HashSet<>(asList(thingTypeB, thingTypeC)));
+ registerService(discoveryParticipantB);
+
+ assertThat(usbSerialDiscoveryService.getSupportedThingTypes(),
+ containsInAnyOrder(thingTypeA, thingTypeB, thingTypeC));
+ }
+
+ @Test
+ public void testThingsAreActuallyDiscovered() {
+ // register one discovery listener
+ DiscoveryListener discoveryListener = mock(DiscoveryListener.class);
+ usbSerialDiscoveryService.addDiscoveryListener(discoveryListener);
+
+ // register two discovery participants
+ UsbSerialDiscoveryParticipant discoveryParticipantA = mock(UsbSerialDiscoveryParticipant.class);
+ registerService(discoveryParticipantA);
+
+ UsbSerialDiscoveryParticipant discoveryParticipantB = mock(UsbSerialDiscoveryParticipant.class);
+ registerService(discoveryParticipantB);
+
+ // when no discovery participant supports a newly discovered device, no device is discovered
+ when(discoveryParticipantA.createResult(any())).thenReturn(null);
+ when(discoveryParticipantB.createResult(any())).thenReturn(null);
+ usbSerialDiscoveryService.usbSerialDeviceDiscovered(generateDeviceInfo());
+ verify(discoveryListener, never()).thingDiscovered(any(), any());
+
+ // when only the first discovery participant supports a newly discovered device, the device is discovered
+ UsbSerialDeviceInformation deviceInfoA = generateDeviceInfo();
+ DiscoveryResult discoveryResultA = mock(DiscoveryResult.class);
+ when(discoveryParticipantA.createResult(deviceInfoA)).thenReturn(discoveryResultA);
+ usbSerialDiscoveryService.usbSerialDeviceDiscovered(deviceInfoA);
+ verify(discoveryListener, times(1)).thingDiscovered(usbSerialDiscoveryService, discoveryResultA);
+
+ // when only the second discovery participant supports a newly discovered device, the device is also discovered
+ UsbSerialDeviceInformation deviceInfoB = generateDeviceInfo();
+ DiscoveryResult discoveryResultB = mock(DiscoveryResult.class);
+ when(discoveryParticipantA.createResult(deviceInfoB)).thenReturn(discoveryResultB);
+ usbSerialDiscoveryService.usbSerialDeviceDiscovered(deviceInfoB);
+ verify(discoveryListener, times(1)).thingDiscovered(usbSerialDiscoveryService, discoveryResultB);
+ }
+
+ @Test
+ public void testDiscoveredThingsAreRemoved() {
+ // register one discovery listener
+ DiscoveryListener discoveryListener = mock(DiscoveryListener.class);
+ usbSerialDiscoveryService.addDiscoveryListener(discoveryListener);
+
+ // register one discovery participant
+ UsbSerialDiscoveryParticipant discoveryParticipant = mock(UsbSerialDiscoveryParticipant.class);
+ registerService(discoveryParticipant);
+
+ // when the discovery participant does not support a removed device, no discovery result is removed
+ when(discoveryParticipant.createResult(any())).thenReturn(null);
+ usbSerialDiscoveryService.usbSerialDeviceRemoved(generateDeviceInfo());
+ verify(discoveryListener, never()).thingRemoved(any(), any());
+
+ // when the first discovery participant supports a removed device, the discovery result is removed
+ UsbSerialDeviceInformation deviceInfo = generateDeviceInfo();
+ ThingUID thingUID = mock(ThingUID.class);
+ when(discoveryParticipant.getThingUID(deviceInfo)).thenReturn(thingUID);
+ usbSerialDiscoveryService.usbSerialDeviceRemoved(deviceInfo);
+ verify(discoveryListener, times(1)).thingRemoved(usbSerialDiscoveryService, thingUID);
+ }
+
+ @Test
+ public void testAddingDiscoveryParticipantAfterAddingUsbDongle() {
+ UsbSerialDeviceInformation usb1 = generateDeviceInfo();
+ UsbSerialDeviceInformation usb2 = generateDeviceInfo();
+ UsbSerialDeviceInformation usb3 = generateDeviceInfo();
+
+ // get info about three added and one removed USB dongles from UsbSerialDiscovery
+ usbSerialDiscoveryService.usbSerialDeviceDiscovered(usb1);
+ usbSerialDiscoveryService.usbSerialDeviceDiscovered(usb2);
+ usbSerialDiscoveryService.usbSerialDeviceRemoved(usb1);
+ usbSerialDiscoveryService.usbSerialDeviceDiscovered(usb3);
+
+ // register one discovery participant
+ UsbSerialDiscoveryParticipant discoveryParticipant = mock(UsbSerialDiscoveryParticipant.class);
+ registerService(discoveryParticipant);
+
+ // then this discovery participant is informed about USB devices usb2 and usb3, but not about usb1
+ verify(discoveryParticipant, never()).createResult(usb1);
+ verify(discoveryParticipant, times(1)).createResult(usb2);
+ verify(discoveryParticipant, times(1)).createResult(usb3);
+ }
+
+ private void setBackgroundDiscovery(boolean status) throws IOException, InterruptedException {
+ ConfigurationAdmin configAdmin = getService(ConfigurationAdmin.class);
+ Configuration configuration = configAdmin.getConfiguration("discovery.usbserial");
+ Hashtable properties = new Hashtable<>();
+ properties.put(CONFIG_PROPERTY_BACKGROUND_DISCOVERY, Boolean.valueOf(status));
+ configuration.update(properties);
+
+ // wait until the configuration is actually set in the usbSerialDiscoveryService
+ waitForAssert(() -> {
+ assertThat(usbSerialDiscoveryService.isBackgroundDiscoveryEnabled(), is(status));
+ }, 1000, 100);
+ }
+
+ private UsbSerialDeviceInformation generateDeviceInfo() {
+ return usbSerialDeviceInformationGenerator.generate();
+ }
+
+}
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/testutil/UsbSerialDeviceInformationGenerator.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/testutil/UsbSerialDeviceInformationGenerator.java
new file mode 100644
index 00000000000..dac7bc38991
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/testutil/UsbSerialDeviceInformationGenerator.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.eclipse.smarthome.config.discovery.usbserial.testutil;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDeviceInformation;
+
+/**
+ * Generate simple instances of {@link UsbSerialDeviceInformation} that can be used in tests.
+ *
+ * @author Henning Sudbrock - initial contribution
+ */
+public class UsbSerialDeviceInformationGenerator {
+
+ private final AtomicInteger counter = new AtomicInteger(0);
+
+ public UsbSerialDeviceInformation generate() {
+ int i = counter.getAndIncrement();
+ return new UsbSerialDeviceInformation(i, i, "serialNumber-" + i, "manufacturer-" + i, "product-" + i, i,
+ "interface-" + i, "ttyUSB" + i);
+ }
+
+}
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/.classpath b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/.classpath
new file mode 100644
index 00000000000..7f457fa4138
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/.classpath
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/.project b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/.project
new file mode 100644
index 00000000000..135f96a6ea1
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/.project
@@ -0,0 +1,33 @@
+
+
+ org.eclipse.smarthome.config.discovery.usbserial
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.pde.ManifestBuilder
+
+
+
+
+ org.eclipse.pde.SchemaBuilder
+
+
+
+
+ org.eclipse.pde.ds.core.builder
+
+
+
+
+
+ org.eclipse.pde.PluginNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/.settings/org.eclipse.jdt.core.prefs b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..0c68a61dca8
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,7 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/META-INF/MANIFEST.MF b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/META-INF/MANIFEST.MF
new file mode 100644
index 00000000000..7c7ff332342
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/META-INF/MANIFEST.MF
@@ -0,0 +1,19 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Eclipse SmartHome Configuration USB-Serial Discovery
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Bundle-SymbolicName: org.eclipse.smarthome.config.discovery.usbserial
+Bundle-Vendor: Eclipse.org/SmartHome
+Bundle-Version: 0.10.0.qualifier
+Export-Package:
+ org.eclipse.smarthome.config.discovery.usbserial
+Import-Package:
+ org.eclipse.jdt.annotation;resolution:=optional,
+ org.eclipse.smarthome.config.discovery,
+ org.eclipse.smarthome.config.discovery.usbserial,
+ org.eclipse.smarthome.core.thing,
+ org.osgi.framework,
+ org.osgi.service.component,
+ org.slf4j
+Service-Component: OSGI-INF/*.xml
+Eclipse-ExtensibleAPI: true
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/NOTICE b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/NOTICE
new file mode 100644
index 00000000000..b8675cd02e8
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/NOTICE
@@ -0,0 +1,19 @@
+This content is produced and maintained by the Eclipse SmartHome project.
+
+* Project home: https://eclipse.org/smarthome/
+
+== 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/eclipse/smarthome
+
+== Copyright Holders
+
+See the NOTICE file distributed with the source code at
+https://github.com/eclipse/smarthome/blob/master/NOTICE
+for detailed information regarding copyright ownership.
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/OSGI-INF/.gitignore b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/OSGI-INF/.gitignore
new file mode 100644
index 00000000000..b878e882aca
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/OSGI-INF/.gitignore
@@ -0,0 +1 @@
+/*.xml
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/build.properties b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/build.properties
new file mode 100644
index 00000000000..2aec6ad17b1
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/build.properties
@@ -0,0 +1,6 @@
+source.. = src/main/java/
+output.. = target/classes/
+bin.includes = META-INF/,\
+ .,\
+ OSGI-INF/,\
+ NOTICE
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/pom.xml b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/pom.xml
new file mode 100644
index 00000000000..70d41d7d131
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/pom.xml
@@ -0,0 +1,18 @@
+
+
+
+ 4.0.0
+
+
+ config
+ org.eclipse.smarthome.bundles
+ 0.10.0-SNAPSHOT
+
+ org.eclipse.smarthome.config
+ org.eclipse.smarthome.config.discovery.usbserial
+
+ eclipse-plugin
+
+ Eclipse SmartHome Configuration USB-Serial Discovery
+
+
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDeviceInformation.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDeviceInformation.java
new file mode 100644
index 00000000000..96c298707a3
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDeviceInformation.java
@@ -0,0 +1,203 @@
+/**
+ * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.eclipse.smarthome.config.discovery.usbserial;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * This is a data container for information about a USB device and the serial port that can be
+ * used to access the device using a serial interface.
+ *
+ * It contains, on the one hand, information from the USB standard device descriptor and standard interface descriptor, and, on
+ * the other hand, the name of the serial port (for Linux, this would be, e.g., '/dev/ttyUSB0', for Windows, e.g.,
+ * 'COM4').
+ *
+ * @author Henning Sudbrock - initial contribution
+ */
+@NonNullByDefault
+public class UsbSerialDeviceInformation {
+
+ private final int vendorId;
+ private final int productId;
+
+ @Nullable
+ private final String serialNumber;
+ @Nullable
+ private final String manufacturer;
+ @Nullable
+ private final String product;
+
+ private final int interfaceNumber;
+ @Nullable
+ private final String interfaceDescription;
+
+ private final String serialPort;
+
+ public UsbSerialDeviceInformation(int vendorId, int productId, @Nullable String serialNumber,
+ @Nullable String manufacturer, @Nullable String product, int interfaceNumber,
+ @Nullable String interfaceDescription, String serialPort) {
+ this.vendorId = requireNonNull(vendorId);
+ this.productId = requireNonNull(productId);
+
+ this.serialNumber = serialNumber;
+ this.manufacturer = manufacturer;
+ this.product = product;
+
+ this.interfaceNumber = interfaceNumber;
+ this.interfaceDescription = interfaceDescription;
+
+ this.serialPort = requireNonNull(serialPort);
+ }
+
+ /**
+ * @return The vendor ID of the USB device (field 'idVendor' in the USB standard device descriptor).
+ */
+ public int getVendorId() {
+ return vendorId;
+ }
+
+ /**
+ * @return The product ID of the USB device (field 'idProduct' in the USB standard device descriptor).
+ */
+ public int getProductId() {
+ return productId;
+ }
+
+ /**
+ * @return The serial number of the USB device (field 'iSerialNumber' in the USB standard device descriptor).
+ */
+ @Nullable
+ public String getSerialNumber() {
+ return serialNumber;
+ }
+
+ /**
+ * @return The manufacturer of the USB device (field 'iManufacturer' in the USB standard device descriptor).
+ */
+ @Nullable
+ public String getManufacturer() {
+ return manufacturer;
+ }
+
+ /**
+ * @return The product description of the USB device (field 'iProduct' in the USB standard device descriptor).
+ */
+ @Nullable
+ public String getProduct() {
+ return product;
+ }
+
+ /**
+ * @return The interface number of the used USB interface (field 'bInterfaceNumber' in the USB standard interface
+ * descriptor).
+ */
+ public int getInterfaceNumber() {
+ return interfaceNumber;
+ }
+
+ /**
+ * @return Description of the used USB interface (field 'iInterface' in the USB standard interface descriptor).
+ */
+ @Nullable
+ public String getInterfaceDescription() {
+ return interfaceDescription;
+ }
+
+ /**
+ * @return The name of the serial port assigned to the USB device. Examples: /dev/ttyUSB1, COM4
+ */
+ public String getSerialPort() {
+ return serialPort;
+ }
+
+ @SuppressWarnings("null")
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + vendorId;
+ result = prime * result + productId;
+ result = prime * result + interfaceNumber;
+ result = prime * result + serialPort.hashCode();
+ result = prime * result + ((manufacturer == null) ? 0 : manufacturer.hashCode());
+ result = prime * result + ((product == null) ? 0 : product.hashCode());
+ result = prime * result + ((serialNumber == null) ? 0 : serialNumber.hashCode());
+ result = prime * result + ((interfaceDescription == null) ? 0 : interfaceDescription.hashCode());
+ return result;
+ }
+
+ @SuppressWarnings("null")
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ UsbSerialDeviceInformation other = (UsbSerialDeviceInformation) obj;
+
+ if (vendorId != other.vendorId) {
+ return false;
+ }
+
+ if (productId != other.productId) {
+ return false;
+ }
+
+ if (interfaceNumber != other.interfaceNumber) {
+ return false;
+ }
+
+ if (!serialPort.equals(other.serialPort)) {
+ return false;
+ }
+
+ if (!Objects.equals(manufacturer, other.manufacturer)) {
+ return false;
+ }
+
+ if (!Objects.equals(product, other.product)) {
+ return false;
+ }
+
+ if (!Objects.equals(serialNumber, other.serialNumber)) {
+ return false;
+ }
+
+ if (!Objects.equals(interfaceDescription, other.interfaceDescription)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "UsbSerialDeviceInformation [vendorId=0x%04X, productId=0x%04X, serialNumber=%s, manufacturer=%s, "
+ + "product=%s, interfaceNumber=0x%02X, interfaceDescription=%s, serialPort=%s]",
+ vendorId, productId, serialNumber, manufacturer, product, interfaceNumber, interfaceDescription,
+ serialPort);
+ }
+
+}
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDiscovery.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDiscovery.java
new file mode 100644
index 00000000000..09425b13145
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDiscovery.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.eclipse.smarthome.config.discovery.usbserial;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.config.discovery.usbserial.internal.UsbSerialDiscoveryService;
+
+/**
+ * Interface for implementations for discovering serial ports provided by a USB device. An implementation of this
+ * interface is required by the {@link UsbSerialDiscoveryService}.
+ *
+ * @author Henning Sudbrock - initial contribution
+ */
+@NonNullByDefault
+public interface UsbSerialDiscovery {
+
+ /**
+ * Executes a single scan for serial ports provided by USB devices; informs listeners about all discovered devices
+ * (including those discovered in a previous scan).
+ */
+ void doSingleScan();
+
+ /**
+ * Starts scanning for serial ports provided by USB devices in the background; informs listeners about newly
+ * discovered devices. Should return fast.
+ */
+ void startBackgroundScanning();
+
+ /**
+ * Stops scanning for serial ports provided by USB devices in the background. Should return fast.
+ */
+ void stopBackgroundScanning();
+
+ /**
+ * Registers an {@link UsbSerialDiscoveryListener} that is then notified about discovered serial ports and USB
+ * devices.
+ */
+ void registerDiscoveryListener(UsbSerialDiscoveryListener listener);
+
+ /**
+ * Unregisters an {@link UsbSerialDiscoveryListener}.
+ */
+ void unregisterDiscoveryListener(UsbSerialDiscoveryListener listener);
+
+}
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDiscoveryListener.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDiscoveryListener.java
new file mode 100644
index 00000000000..e1d2730fa84
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDiscoveryListener.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.eclipse.smarthome.config.discovery.usbserial;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Listener interface for {@link UsbSerialDiscovery}s.
+ *
+ * @author Henning Sudbrock - initial contribution
+ */
+@NonNullByDefault
+public interface UsbSerialDiscoveryListener {
+
+ /**
+ * Called when a new serial port provided by a USB device is discovered.
+ */
+ void usbSerialDeviceDiscovered(UsbSerialDeviceInformation usbSerialDeviceInformation);
+
+ /**
+ * Called when a serial port provided by a USB device has been removed.
+ */
+ void usbSerialDeviceRemoved(UsbSerialDeviceInformation usbSerialDeviceInformation);
+
+}
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDiscoveryParticipant.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDiscoveryParticipant.java
new file mode 100644
index 00000000000..1149a3eae99
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/UsbSerialDiscoveryParticipant.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.eclipse.smarthome.config.discovery.usbserial;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.config.discovery.DiscoveryResult;
+import org.eclipse.smarthome.config.discovery.usbserial.internal.UsbSerialDiscoveryService;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.ThingUID;
+
+/**
+ * A {@link UsbSerialDiscoveryParticipant} that is registered as a component is picked up by the
+ * {@link UsbSerialDiscoveryService} and can thus contribute {@link DiscoveryResult}s from
+ * scans for USB devices with an associated serial port.
+ *
+ * @author Henning Sudbrock - initial contribution
+ */
+@NonNullByDefault
+public interface UsbSerialDiscoveryParticipant {
+
+ /**
+ * Defines the list of thing types that this participant can identify.
+ *
+ * @return a set of thing type UIDs for which results can be created
+ */
+ public Set getSupportedThingTypeUIDs();
+
+ /**
+ * Creates a discovery result for a USB device with corresponding serial port.
+ *
+ * @param deviceInformation information about the USB device and the corresponding serial port
+ * @return the according discovery result or null
if the device is not
+ * supported by this participant
+ */
+ @Nullable
+ public DiscoveryResult createResult(UsbSerialDeviceInformation deviceInformation);
+
+ /**
+ * Returns the thing UID for a USB device with corresponding serial port.
+ *
+ * @param deviceInformation information about the USB device and the corresponding serial port
+ * @return a thing UID or null
if the device is not supported
+ * by this participant
+ */
+ @Nullable
+ public ThingUID getThingUID(UsbSerialDeviceInformation deviceInformation);
+}
diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/internal/UsbSerialDiscoveryService.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/internal/UsbSerialDiscoveryService.java
new file mode 100644
index 00000000000..022deffb231
--- /dev/null
+++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/internal/UsbSerialDiscoveryService.java
@@ -0,0 +1,186 @@
+/**
+ * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.eclipse.smarthome.config.discovery.usbserial.internal;
+
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService;
+import org.eclipse.smarthome.config.discovery.DiscoveryResult;
+import org.eclipse.smarthome.config.discovery.DiscoveryService;
+import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDeviceInformation;
+import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDiscovery;
+import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDiscoveryListener;
+import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDiscoveryParticipant;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link DiscoveryService} for discovering USB devices with an associated serial port.
+ *
+ * This discovery service is intended to be used by bindings that support USB devices, but do not directly talk to the
+ * USB devices but rather use a serial port for the communication, where the serial port is provided by an operating
+ * system driver outside the scope of Eclipse SmartHome. Examples for such USB devices are USB dongles that provide
+ * access to wireless networks, like, e.g., Zigbeee or Zwave dongles.
+ *
+ * This discovery service provides functionality for discovering added and removed USB devices and the corresponding
+ * serial ports. The actual {@link DiscoveryResult}s are then provided by {@link UsbSerialDiscoveryParticipant}s, which
+ * are called by this discovery service whenever new devices are detected or devices are removed. Such
+ * {@link UsbSerialDiscoveryParticipant}s should be provided by bindings accessing USB devices via a serial port.
+ *
+ * This discovery service requires a component implementing the interface {@link UsbSerialDiscovery}, which performs the
+ * actual serial port and USB device discovery (as this discovery might differ depending on the operating system).
+ *
+ * @author Henning Sudbrock - initial contribution
+ */
+@NonNullByDefault
+@Component(immediate = true, service = { DiscoveryService.class,
+ UsbSerialDiscoveryService.class }, configurationPid = "discovery.usbserial")
+public class UsbSerialDiscoveryService extends AbstractDiscoveryService implements UsbSerialDiscoveryListener {
+
+ private final Logger logger = LoggerFactory.getLogger(UsbSerialDiscoveryService.class);
+
+ private final Set discoveryParticipants = new CopyOnWriteArraySet<>();
+
+ private final Set previouslyDiscovered = new CopyOnWriteArraySet<>();
+
+ @NonNullByDefault({})
+ private UsbSerialDiscovery usbSerialDiscovery;
+
+ public UsbSerialDiscoveryService() {
+ super(5);
+ }
+
+ @Override
+ @Activate
+ protected void activate(@Nullable Map configProperties) {
+ super.activate(configProperties);
+ usbSerialDiscovery.registerDiscoveryListener(this);
+ if (isBackgroundDiscoveryEnabled()) {
+ usbSerialDiscovery.startBackgroundScanning();
+ }
+ }
+
+ @Modified
+ @Override
+ protected void modified(@Nullable Map<@NonNull String, @Nullable Object> configProperties) {
+ super.modified(configProperties);
+ }
+
+ @Override
+ @Deactivate
+ protected void deactivate() {
+ super.deactivate();
+ }
+
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+ protected void addUsbSerialDiscoveryParticipant(UsbSerialDiscoveryParticipant participant) {
+ this.discoveryParticipants.add(participant);
+ for (UsbSerialDeviceInformation usbSerialDeviceInformation : previouslyDiscovered) {
+ DiscoveryResult result = participant.createResult(usbSerialDeviceInformation);
+ if (result != null) {
+ thingDiscovered(result);
+ }
+ }
+ }
+
+ protected void removeUsbSerialDiscoveryParticipant(UsbSerialDiscoveryParticipant participant) {
+ this.discoveryParticipants.remove(participant);
+ }
+
+ @Reference
+ protected void setUsbSerialDiscovery(UsbSerialDiscovery usbSerialDiscovery) {
+ this.usbSerialDiscovery = usbSerialDiscovery;
+ }
+
+ protected synchronized void unsetUsbSerialDiscovery(UsbSerialDiscovery usbSerialDiscovery) {
+ usbSerialDiscovery.stopBackgroundScanning();
+ usbSerialDiscovery.unregisterDiscoveryListener(this);
+ this.usbSerialDiscovery = null;
+ this.previouslyDiscovered.clear();
+ }
+
+ @Override
+ public Set getSupportedThingTypes() {
+ return discoveryParticipants.stream().flatMap(participant -> participant.getSupportedThingTypeUIDs().stream())
+ .collect(toSet());
+ }
+
+ @Override
+ protected void startScan() {
+ if (usbSerialDiscovery != null) {
+ usbSerialDiscovery.doSingleScan();
+ } else {
+ logger.info("Could not scan, as there is no USB-Serial discovery service configured.");
+ }
+ }
+
+ @Override
+ protected void startBackgroundDiscovery() {
+ if (usbSerialDiscovery != null) {
+ usbSerialDiscovery.startBackgroundScanning();
+ } else {
+ logger.info(
+ "Could not start background discovery, as there is no USB-Serial discovery service configured.");
+ }
+ }
+
+ @Override
+ protected void stopBackgroundDiscovery() {
+ if (usbSerialDiscovery != null) {
+ usbSerialDiscovery.stopBackgroundScanning();
+ } else {
+ logger.info("Could not stop background discovery, as there is no USB-Serial discovery service configured.");
+ }
+ }
+
+ @Override
+ public void usbSerialDeviceDiscovered(UsbSerialDeviceInformation usbSerialDeviceInformation) {
+ logger.debug("Discovered new USB-Serial device: {}", usbSerialDeviceInformation);
+ previouslyDiscovered.add(usbSerialDeviceInformation);
+ for (UsbSerialDiscoveryParticipant participant : discoveryParticipants) {
+ DiscoveryResult result = participant.createResult(usbSerialDeviceInformation);
+ if (result != null) {
+ thingDiscovered(result);
+ }
+ }
+ }
+
+ @Override
+ public void usbSerialDeviceRemoved(UsbSerialDeviceInformation usbSerialDeviceInformation) {
+ logger.debug("Discovered removed USB-Serial device: {}", usbSerialDeviceInformation);
+ previouslyDiscovered.remove(usbSerialDeviceInformation);
+ for (UsbSerialDiscoveryParticipant participant : discoveryParticipants) {
+ ThingUID thingUID = participant.getThingUID(usbSerialDeviceInformation);
+ if (thingUID != null) {
+ thingRemoved(thingUID);
+ }
+ }
+ }
+
+}
diff --git a/bundles/config/pom.xml b/bundles/config/pom.xml
index 97c71d4dd0c..3e6f12b83bc 100644
--- a/bundles/config/pom.xml
+++ b/bundles/config/pom.xml
@@ -24,6 +24,10 @@
org.eclipse.smarthome.config.discovery.mdns
org.eclipse.smarthome.config.discovery.test
org.eclipse.smarthome.config.discovery.upnp
+ org.eclipse.smarthome.config.discovery.usbserial
+ org.eclipse.smarthome.config.discovery.usbserial.test
+ org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs
+ org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test
org.eclipse.smarthome.config.serial
org.eclipse.smarthome.config.xml
org.eclipse.smarthome.config.xml.test