diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/.classpath b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/.classpath new file mode 100644 index 00000000000..3827701922c --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/.project b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/.project new file mode 100644 index 00000000000..a89488057f9 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/.project @@ -0,0 +1,28 @@ + + + org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.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.linuxsysfs.test/.settings/org.eclipse.jdt.core.prefs b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.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.linuxsysfs.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.linuxsysfs.test/META-INF/MANIFEST.MF b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..249a14e1c19 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/META-INF/MANIFEST.MF @@ -0,0 +1,21 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Eclipse SmartHome Configuration Discovery USB-Serial Linux Sysfs Tests +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-SymbolicName: org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test;singleton:=true +Bundle-Vendor: Eclipse.org/SmartHome +Bundle-Version: 0.10.0.qualifier +Fragment-Host: org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs +Import-Package: + org.eclipse.jdt.annotation;resolution:=optional, + org.eclipse.smarthome.config.discovery.usbserial.testutil, + 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 diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/NOTICE b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/NOTICE new file mode 100644 index 00000000000..b8675cd02e8 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.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.linuxsysfs.test/build.properties b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/build.properties new file mode 100644 index 00000000000..0c65482c5cc --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.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.linuxsysfs.test/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test.launch b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test.launch new file mode 100644 index 00000000000..04ee68d5e7a --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test.launch @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/pom.xml b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/pom.xml new file mode 100644 index 00000000000..299a30fff55 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + config + org.eclipse.smarthome.bundles + 0.10.0-SNAPSHOT + + org.eclipse.smarthome.config + org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test + eclipse-test-plugin + Eclipse SmartHome Configuration USB-Serial Discovery for Linux using sysfs scanning Tests + + + org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test + org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test + + + + + + ${tycho-groupid} + target-platform-configuration + + + + + + eclipse-plugin + org.eclipse.equinox.ds + 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.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.linuxsysfs.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/DeltaUsbSerialScannerTest.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/DeltaUsbSerialScannerTest.java new file mode 100644 index 00000000000..58b9a8e7340 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/DeltaUsbSerialScannerTest.java @@ -0,0 +1,102 @@ +/** + * 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.util.Arrays.asList; +import static java.util.Collections.emptySet; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.HashSet; + +import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDeviceInformation; +import org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.internal.DeltaUsbSerialScanner.Delta; +import org.eclipse.smarthome.config.discovery.usbserial.testutil.UsbSerialDeviceInformationGenerator; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for the {@link DeltaUsbSerialScanner}. + * + * @author Henning Sudbrock - initial contribution + */ +public class DeltaUsbSerialScannerTest { + + UsbSerialDeviceInformationGenerator usbDeviceInfoGenerator = new UsbSerialDeviceInformationGenerator(); + + private UsbSerialScanner usbSerialScanner; + private DeltaUsbSerialScanner deltaUsbSerialScanner; + + @Before + public void setup() { + usbSerialScanner = mock(UsbSerialScanner.class); + deltaUsbSerialScanner = new DeltaUsbSerialScanner(usbSerialScanner); + } + + /** + * If there are no devices discovered in a first scan, then there is no delta. + */ + @Test + public void testInitialEmptyResult() throws IOException { + when(usbSerialScanner.scan()).thenReturn(emptySet()); + + Delta delta = deltaUsbSerialScanner.scan(); + + assertThat(delta.getAdded(), is(empty())); + assertThat(delta.getRemoved(), is(empty())); + assertThat(delta.getUnchanged(), is(empty())); + } + + /** + * If there are devices discovered in a first scan, then all devices are in the 'added' section of the delta. + */ + @Test + public void testInitialNonEmptyResult() throws IOException { + UsbSerialDeviceInformation usb1 = usbDeviceInfoGenerator.generate(); + UsbSerialDeviceInformation usb2 = usbDeviceInfoGenerator.generate(); + when(usbSerialScanner.scan()).thenReturn(new HashSet<>(asList(usb1, usb2))); + + Delta delta = deltaUsbSerialScanner.scan(); + + assertThat(delta.getAdded(), containsInAnyOrder(usb1, usb2)); + assertThat(delta.getRemoved(), is(empty())); + assertThat(delta.getUnchanged(), is(empty())); + } + + /** + * If a first scan discovers devices usb1 and usb2, and a second scan discovers devices usb2 and usb3, then the + * delta for the second scan is: usb3 is added, usb1 is removed, and usb2 is unchanged. + */ + @Test + public void testDevicesAddedAndRemovedAndUnchanged() throws IOException { + UsbSerialDeviceInformation usb1 = usbDeviceInfoGenerator.generate(); + UsbSerialDeviceInformation usb2 = usbDeviceInfoGenerator.generate(); + UsbSerialDeviceInformation usb3 = usbDeviceInfoGenerator.generate(); + + when(usbSerialScanner.scan()).thenReturn(new HashSet<>(asList(usb1, usb2))); + deltaUsbSerialScanner.scan(); + + when(usbSerialScanner.scan()).thenReturn(new HashSet<>(asList(usb2, usb3))); + Delta delta = deltaUsbSerialScanner.scan(); + + assertThat(delta.getAdded(), contains(usb3)); + assertThat(delta.getRemoved(), contains(usb1)); + assertThat(delta.getUnchanged(), contains(usb2)); + } + +} diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/PollingUsbSerialScannerTest.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/PollingUsbSerialScannerTest.java new file mode 100644 index 00000000000..a916f4cee2a --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/PollingUsbSerialScannerTest.java @@ -0,0 +1,140 @@ +/** + * 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.util.Arrays.asList; +import static org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.internal.PollingUsbSerialScanner.PAUSE_BETWEEN_SCANS_IN_SECONDS_ATTRIBUTE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDeviceInformation; +import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDiscoveryListener; +import org.eclipse.smarthome.config.discovery.usbserial.testutil.UsbSerialDeviceInformationGenerator; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for the {@link PollingUsbSerialScanner}. + * + * @author Henning Sudbrock - initial contribution + */ +public class PollingUsbSerialScannerTest { + + UsbSerialDeviceInformationGenerator usbDeviceInfoGenerator = new UsbSerialDeviceInformationGenerator(); + + UsbSerialScanner usbSerialScanner; + PollingUsbSerialScanner pollingScanner; + UsbSerialDiscoveryListener discoveryListener; + + @Before + public void setup() { + usbSerialScanner = mock(UsbSerialScanner.class); + pollingScanner = new PollingUsbSerialScanner(); + pollingScanner.setUsbSerialScanner(usbSerialScanner); + + discoveryListener = mock(UsbSerialDiscoveryListener.class); + pollingScanner.registerDiscoveryListener(discoveryListener); + + Map config = new HashMap<>(); + config.put(PAUSE_BETWEEN_SCANS_IN_SECONDS_ATTRIBUTE, "1"); + pollingScanner.modified(config); + } + + @Test + public void testNoScansWithoutBackgroundDiscovery() throws IOException, InterruptedException { + // Wait a little more than one second to give background scanning a chance to kick in. + Thread.sleep(1200); + + verify(usbSerialScanner, never()).scan(); + } + + @Test + public void testSingleScanReportsResultsCorrectAfterOneScan() throws IOException { + UsbSerialDeviceInformation usb1 = usbDeviceInfoGenerator.generate(); + UsbSerialDeviceInformation usb2 = usbDeviceInfoGenerator.generate(); + UsbSerialDeviceInformation usb3 = usbDeviceInfoGenerator.generate(); + + when(usbSerialScanner.scan()).thenReturn(new HashSet<>(asList(usb1, usb2))); + + pollingScanner.doSingleScan(); + + // Expectation: discovery listener called with newly discovered devices usb1 and usb2; not called with removed + // devices. + + verify(discoveryListener, times(1)).usbSerialDeviceDiscovered(usb1); + verify(discoveryListener, times(1)).usbSerialDeviceDiscovered(usb2); + verify(discoveryListener, never()).usbSerialDeviceDiscovered(usb3); + + verify(discoveryListener, never()).usbSerialDeviceRemoved(any(UsbSerialDeviceInformation.class)); + } + + @Test + public void testSingleScanReportsResultsCorrectlyAfterTwoScans() throws IOException { + UsbSerialDeviceInformation usb1 = usbDeviceInfoGenerator.generate(); + UsbSerialDeviceInformation usb2 = usbDeviceInfoGenerator.generate(); + UsbSerialDeviceInformation usb3 = usbDeviceInfoGenerator.generate(); + + when(usbSerialScanner.scan()).thenReturn(new HashSet<>(asList(usb1, usb2))) + .thenReturn(new HashSet<>(asList(usb2, usb3))); + + pollingScanner.unregisterDiscoveryListener(discoveryListener); + pollingScanner.doSingleScan(); + + pollingScanner.registerDiscoveryListener(discoveryListener); + pollingScanner.doSingleScan(); + + // Expectation: discovery listener called once for removing usb1, and once for adding usb2/usb3 each. + + verify(discoveryListener, never()).usbSerialDeviceDiscovered(usb1); + verify(discoveryListener, times(1)).usbSerialDeviceRemoved(usb1); + + verify(discoveryListener, times(1)).usbSerialDeviceDiscovered(usb2); + verify(discoveryListener, never()).usbSerialDeviceRemoved(usb2); + + verify(discoveryListener, times(1)).usbSerialDeviceDiscovered(usb3); + verify(discoveryListener, never()).usbSerialDeviceRemoved(usb3); + } + + @Test + public void testBackgroundScanning() throws IOException, InterruptedException { + UsbSerialDeviceInformation usb1 = usbDeviceInfoGenerator.generate(); + UsbSerialDeviceInformation usb2 = usbDeviceInfoGenerator.generate(); + UsbSerialDeviceInformation usb3 = usbDeviceInfoGenerator.generate(); + + when(usbSerialScanner.scan()).thenReturn(new HashSet<>(asList(usb1, usb2))) + .thenReturn(new HashSet<>(asList(usb2, usb3))); + + pollingScanner.startBackgroundScanning(); + + Thread.sleep(1500); + + pollingScanner.stopBackgroundScanning(); + + // Expectation: discovery listener called once for each discovered device, and once for removal of usb1. + + verify(discoveryListener, times(1)).usbSerialDeviceDiscovered(usb1); + verify(discoveryListener, times(1)).usbSerialDeviceRemoved(usb1); + + verify(discoveryListener, times(1)).usbSerialDeviceDiscovered(usb2); + verify(discoveryListener, never()).usbSerialDeviceRemoved(usb2); + + verify(discoveryListener, times(1)).usbSerialDeviceDiscovered(usb3); + verify(discoveryListener, never()).usbSerialDeviceRemoved(usb3); + } + +} diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/SysFsUsbSerialScannerTest.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/SysFsUsbSerialScannerTest.java new file mode 100644 index 00000000000..acfef8bd576 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.test/src/test/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/SysFsUsbSerialScannerTest.java @@ -0,0 +1,224 @@ +/** + * 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.lang.Integer.toHexString; +import static java.nio.file.Files.*; +import static java.nio.file.attribute.PosixFilePermission.*; +import static java.util.Arrays.asList; +import static org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs.internal.SysfsUsbSerialScanner.*; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeTrue; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDeviceInformation; +import org.hamcrest.Matcher; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * Unit tests for the {@link SysfsUsbSerialScanner}. + * + * @author Henning Sudbrock - initial contribution + */ +public class SysFsUsbSerialScannerTest { + + @Rule + public final TemporaryFolder rootFolder = new TemporaryFolder(); + + private static final String SYSFS_TTY_DEVICES_DIR = "sys/class/tty"; + private static final String DEV_DIR = "dev"; + private static final String SYSFS_USB_DEVICES_DIR = "sys/devices/pci0000:00/0000:00:14.0/usb1"; + + private SysfsUsbSerialScanner scanner; + + private Path rootPath; + private Path devPath; + private Path sysfsTtyPath; + private Path sysfsUsbPath; + + private int deviceIndexCounter = 0; + + @Before + public void setup() throws IOException { + // only run the tests on systems that support symbolic links + assumeTrue(systemSupportsSymLinks()); + + rootPath = rootFolder.getRoot().toPath(); + + devPath = rootPath.resolve(DEV_DIR); + createDirectories(devPath); + + sysfsTtyPath = rootPath.resolve(SYSFS_TTY_DEVICES_DIR); + createDirectories(sysfsTtyPath); + + sysfsUsbPath = rootPath.resolve(SYSFS_USB_DEVICES_DIR); + createDirectories(sysfsUsbPath); + + scanner = new SysfsUsbSerialScanner(); + + Map config = new HashMap<>(); + config.put(SYSFS_TTY_DEVICES_DIRECTORY_ATTRIBUTE, rootPath.resolve(SYSFS_TTY_DEVICES_DIR)); + config.put(DEV_DIRECTORY_ATTRIBUTE, rootPath.resolve(DEV_DIR)); + scanner.modified(config); + } + + @Test(expected = IOException.class) + public void testIOExceptionIfSysfsTtyDoesNotExist() throws IOException { + delete(sysfsTtyPath); + scanner.scan(); + } + + @Test + public void testNoResultsIfNoTtyDevicesExist() throws IOException { + assertThat(scanner.scan(), is(empty())); + } + + @Test + public void testUsbSerialDevicesAreCorrectlyIdentified() throws IOException { + createDevice("ttyUSB0", 0xABCD, 0X1234, "sample manufacturer", "sample product", "123-456-789", 0, + "sample interface"); + createDevice("ttyUSB1", 0x0001, 0X0002, "another manufacturer", "product desc", "987-654-321", 1, + "another interface"); + + assertThat(scanner.scan(), hasSize(2)); + assertThat(scanner.scan(), hasItem(isUsbSerialDeviceInfo(0xABCD, 0x1234, "123-456-789", "sample manufacturer", + "sample product", 0, "sample interface", rootPath.resolve(DEV_DIR).resolve("ttyUSB0").toString()))); + assertThat(scanner.scan(), hasItem(isUsbSerialDeviceInfo(0x0001, 0X0002, "987-654-321", "another manufacturer", + "product desc", 1, "another interface", rootPath.resolve(DEV_DIR).resolve("ttyUSB1").toString()))); + } + + @Test + public void testNonReadableDeviceFilesAreSkipped() throws IOException { + createDevice("ttyUSB0", 0xABCD, 0X1234, "sample manufacturer", "sample product", "123-456-789", 0, + "interfaceDesc"); + setPosixFilePermissions(devPath.resolve("ttyUSB0"), new HashSet<>(asList(OWNER_WRITE))); + assertThat(scanner.scan(), is(empty())); + } + + @Test + public void testNonWritableDeviceFilesAreSkipped() throws IOException { + createDevice("ttyUSB0", 0xABCD, 0X1234, "sample manufacturer", "sample product", "123-456-789", 0, + "interfaceDesc"); + setPosixFilePermissions(devPath.resolve("ttyUSB0"), new HashSet<>(asList(OWNER_READ))); + assertThat(scanner.scan(), is(empty())); + } + + @Test + public void testDeviceWithoutVendorIdIsSkipped() throws IOException { + createDevice("ttyUSB0", 0xABCD, 0X1234, "sample manufacturer", "sample product", "123-456-789", 0, + "interfaceDesc", DeviceCreationOption.NO_VENDOR_ID); + assertThat(scanner.scan(), is(empty())); + } + + @Test + public void testDeviceWithoutProductIdIsSkipped() throws IOException { + createDevice("ttyUSB0", 0xABCD, 0X1234, "sample manufacturer", "sample product", "123-456-789", 0, + "interfaceDesc", DeviceCreationOption.NO_VENDOR_ID); + assertThat(scanner.scan(), is(empty())); + } + + @Test + public void testDeviceWithoutInterfaceNumberIsSkipped() throws IOException { + createDevice("ttyUSB0", 0xABCD, 0X1234, "sample manufacturer", "sample product", "123-456-789", 0, + "interfaceDesc", DeviceCreationOption.NO_INTERFACE_NUMBER); + assertThat(scanner.scan(), is(empty())); + } + + @Test + public void testNonUsbDeviceIsSkipped() throws IOException { + createDevice("ttyUSB0", 0xABCD, 0X1234, "sample manufacturer", "sample product", "123-456-789", 0, + "interfaceDesc", DeviceCreationOption.NON_USB_DEVICE); + assertThat(scanner.scan(), is(empty())); + } + + private void createDevice(String serialPortName, int vendorId, int productId, String manufacturer, String product, + String serialNumber, int interfaceNumber, String interfaceDescription, + DeviceCreationOption... deviceCreationOptions) throws IOException { + int deviceIndex = deviceIndexCounter++; + + // Create the device file in /dev + createFile(devPath.resolve(serialPortName)); + + // Create the USB device folder structure + Path usbDevicePath = sysfsUsbPath.resolve(String.format("1-%d", deviceIndex)); + Path usbInterfacePath = usbDevicePath.resolve(String.format("1-%d:1.%d", deviceIndex, interfaceNumber)); + Path serialDevicePath = usbInterfacePath.resolve(serialPortName); + createDirectories(serialDevicePath); + + // Create the symlink into the USB device folder structure + if (!Arrays.asList(deviceCreationOptions).contains(DeviceCreationOption.NON_USB_DEVICE)) { + createSymbolicLink(sysfsTtyPath.resolve(serialPortName), serialDevicePath); + } else { + createSymbolicLink(sysfsTtyPath.resolve(serialPortName), devPath); + } + + // Create the files containing information about the USB device + if (!Arrays.asList(deviceCreationOptions).contains(DeviceCreationOption.NO_VENDOR_ID)) { + write(createFile(usbDevicePath.resolve("idVendor")), toHexString(vendorId).getBytes()); + } + if (!Arrays.asList(deviceCreationOptions).contains(DeviceCreationOption.NO_PRODUCT_ID)) { + write(createFile(usbDevicePath.resolve("idProduct")), toHexString(productId).getBytes()); + } + if (manufacturer != null) { + write(createFile(usbDevicePath.resolve("manufacturer")), manufacturer.getBytes()); + } + if (product != null) { + write(createFile(usbDevicePath.resolve("product")), product.getBytes()); + } + if (serialNumber != null) { + write(createFile(usbDevicePath.resolve("serial")), serialNumber.getBytes()); + } + + // Create the files containing information about the USB interface + if (!Arrays.asList(deviceCreationOptions).contains(DeviceCreationOption.NO_INTERFACE_NUMBER)) { + write(createFile(usbInterfacePath.resolve("bInterfaceNumber")), toHexString(interfaceNumber).getBytes()); + } + if (interfaceDescription != null) { + write(createFile(usbInterfacePath.resolve("interface")), interfaceDescription.getBytes()); + } + } + + private Matcher isUsbSerialDeviceInfo(int vendorId, int productId, String serialNumber, + String manufacturer, String product, int interfaceNumber, String interfaceDescription, String serialPort) { + return equalTo(new UsbSerialDeviceInformation(vendorId, productId, serialNumber, manufacturer, product, + interfaceNumber, interfaceDescription, serialPort)); + } + + private boolean systemSupportsSymLinks() throws IOException { + try { + createSymbolicLink(rootFolder.getRoot().toPath().resolve("aSymbolicLink"), rootFolder.getRoot().toPath()); + return true; + } catch (UnsupportedOperationException e) { + return false; + } + } + + private enum DeviceCreationOption { + NO_VENDOR_ID, + NO_PRODUCT_ID, + NO_INTERFACE_NUMBER, + NON_USB_DEVICE; + } +} diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/.classpath b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/.classpath new file mode 100644 index 00000000000..7f457fa4138 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/.project b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/.project new file mode 100644 index 00000000000..bb9ee968664 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/.project @@ -0,0 +1,33 @@ + + + org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs + + + + + + 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.linuxsysfs/.settings/org.eclipse.jdt.core.prefs b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..0c68a61dca8 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/.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.linuxsysfs/META-INF/MANIFEST.MF b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..8b2fef9d26a --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/META-INF/MANIFEST.MF @@ -0,0 +1,15 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Eclipse SmartHome Configuration USB-Serial Discovery Linux sysfs Scanning +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-SymbolicName: org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs +Bundle-Vendor: Eclipse.org/SmartHome +Bundle-Version: 0.10.0.qualifier +Import-Package: + org.eclipse.jdt.annotation;resolution:=optional, + org.eclipse.smarthome.config.discovery.usbserial, + org.eclipse.smarthome.core.common, + org.osgi.framework, + org.osgi.service.component, + org.slf4j +Service-Component: OSGI-INF/*.xml diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/NOTICE b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/NOTICE new file mode 100644 index 00000000000..b8675cd02e8 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/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.linuxsysfs/OSGI-INF/.gitignore b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/OSGI-INF/.gitignore new file mode 100644 index 00000000000..b878e882aca --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/OSGI-INF/.gitignore @@ -0,0 +1 @@ +/*.xml diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/build.properties b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/build.properties new file mode 100644 index 00000000000..2aec6ad17b1 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/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.linuxsysfs/pom.xml b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/pom.xml new file mode 100644 index 00000000000..e51f401ea30 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/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.linuxsysfs + + eclipse-plugin + + Eclipse SmartHome Configuration USB-Serial Discovery for Linux using sysfs scanning + + diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/DeltaUsbSerialScanner.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/DeltaUsbSerialScanner.java new file mode 100644 index 00000000000..7e1127b303d --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/DeltaUsbSerialScanner.java @@ -0,0 +1,95 @@ +/** + * 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.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.config.discovery.usbserial.UsbSerialDeviceInformation; + +/** + * Permits to perform repeated scans for USB devices with associated serial port. Keeps the last scan result as internal + * state, for detecting which devices were added, as well as which devices were removed. + * + * @author Henning Sudbrock - initial contribution + */ +@NonNullByDefault +public class DeltaUsbSerialScanner { + + private Set lastScanResult = new HashSet<>(); + + private final UsbSerialScanner usbSerialScanner; + + public DeltaUsbSerialScanner(UsbSerialScanner usbSerialScanner) { + this.usbSerialScanner = usbSerialScanner; + } + + /** + * Scans for USB-Serial devices, and returns the delta to the last scan result. + *

+ * This method is synchronized to prevent multiple parallel invocations of this method that could bring the value of + * lastScanResult into an inconsistent state. + * + * @return The delta to the last scan result. + * @throws IOException if the scan using the {@link UsbSerialScanner} throws an IOException. + */ + public synchronized Delta scan() throws IOException { + Set scanResult = usbSerialScanner.scan(); + + Set added = setDifference(scanResult, lastScanResult); + Set removed = setDifference(lastScanResult, scanResult); + Set unchanged = setDifference(scanResult, added); + + lastScanResult = scanResult; + + return new Delta<>(added, removed, unchanged); + } + + private Set setDifference(Set set1, Set set2) { + Set result = new HashSet<>(set1); + result.removeAll(set2); + return Collections.unmodifiableSet(result); + } + + /** + * Delta between two subsequent scan results. + */ + class Delta { + + private final Set added; + private final Set removed; + private final Set unchanged; + + public Delta(Set added, Set removed, Set unchanged) { + this.added = added; + this.removed = removed; + this.unchanged = unchanged; + } + + public Set getAdded() { + return added; + } + + public Set getRemoved() { + return removed; + } + + public Set getUnchanged() { + return unchanged; + } + } + +} diff --git a/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/PollingUsbSerialScanner.java b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/PollingUsbSerialScanner.java new file mode 100644 index 00000000000..c92667608ac --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.discovery.usbserial.linuxsysfs/src/main/java/org/eclipse/smarthome/config/discovery/usbserial/linuxsysfs/internal/PollingUsbSerialScanner.java @@ -0,0 +1,177 @@ +/** + * 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.lang.Long.parseLong; + +import java.io.IOException; +import java.time.Duration; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +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.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.linuxsysfs.internal.DeltaUsbSerialScanner.Delta; +import org.eclipse.smarthome.core.common.ThreadPoolManager; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link UsbSerialDiscovery} that implements background discovery by doing repetitive scans using a + * {@link UsbSerialScanner}, pausing a configurable amount of time between subsequent scans. + * + * @author Henning Sudbrock - initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "discovery.usbserial.linuxsysfs.pollingscanner") +public class PollingUsbSerialScanner implements UsbSerialDiscovery { + + private final Logger logger = LoggerFactory.getLogger(PollingUsbSerialScanner.class); + + private static final String THREAD_POOL_NAME = "usb-serial-discovery-linux-sysfs"; + + public static final String PAUSE_BETWEEN_SCANS_IN_SECONDS_ATTRIBUTE = "pauseBetweenScansInSeconds"; + private static final Duration DEFAULT_PAUSE_BETWEEN_SCANS = Duration.ofSeconds(5); + private Duration pauseBetweenScans = DEFAULT_PAUSE_BETWEEN_SCANS; + + @NonNullByDefault({}) + private DeltaUsbSerialScanner deltaUsbSerialScanner; + + private final Set discoveryListeners = new CopyOnWriteArraySet<>(); + + private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(THREAD_POOL_NAME); + @Nullable + private ScheduledFuture backgroundScanningJob; + + @Reference + protected void setUsbSerialScanner(UsbSerialScanner usbSerialScanner) { + deltaUsbSerialScanner = new DeltaUsbSerialScanner(usbSerialScanner); + } + + protected void unsetUsbSerialScanner(UsbSerialScanner usbSerialScanner) { + deltaUsbSerialScanner = null; + } + + @Activate + protected void activate(Map config) { + if (config.containsKey(PAUSE_BETWEEN_SCANS_IN_SECONDS_ATTRIBUTE)) { + pauseBetweenScans = Duration + .ofSeconds(parseLong(config.get(PAUSE_BETWEEN_SCANS_IN_SECONDS_ATTRIBUTE).toString())); + } + } + + @Modified + protected synchronized void modified(Map config) { + if (config.containsKey(PAUSE_BETWEEN_SCANS_IN_SECONDS_ATTRIBUTE)) { + pauseBetweenScans = Duration + .ofSeconds(parseLong(config.get(PAUSE_BETWEEN_SCANS_IN_SECONDS_ATTRIBUTE).toString())); + + if (backgroundScanningJob != null) { + stopBackgroundScanning(); + startBackgroundScanning(); + } + } + } + + /** + * Performs a single scan for newly added and removed devices. + */ + @Override + public void doSingleScan() { + singleScanInternal(true); + } + + /** + * Starts repeatedly scanning for newly added and removed USB devices in the usbserial devices folder (where the + * duration between two subsequent scans is configurable). + *

+ * This repeated scanning can be stopped using {@link #stopBackgroundScanning()}. + */ + @Override + public synchronized void startBackgroundScanning() { + if (backgroundScanningJob == null) { + backgroundScanningJob = scheduler.scheduleWithFixedDelay(() -> { + singleScanInternal(false); + }, 0, pauseBetweenScans.getSeconds(), TimeUnit.SECONDS); + logger.debug("Scheduled USB-Serial background discovery every {} seconds", pauseBetweenScans.getSeconds()); + } + } + + /** + * Stops repeatedly scanning for newly added and removed USB devices. This can be restarted using + * {@link #startBackgroundScanning()}. + */ + @Override + public synchronized void stopBackgroundScanning() { + logger.debug("Stopping USB-Serial background discovery"); + ScheduledFuture currentBackgroundScanningJob = backgroundScanningJob; + if (currentBackgroundScanningJob != null && !currentBackgroundScanningJob.isCancelled()) { + if (currentBackgroundScanningJob.cancel(true)) { + backgroundScanningJob = null; + logger.debug("Stopped USB-serial background discovery"); + } + } + } + + @Override + public void registerDiscoveryListener(UsbSerialDiscoveryListener listener) { + discoveryListeners.add(listener); + } + + @Override + public void unregisterDiscoveryListener(UsbSerialDiscoveryListener listener) { + discoveryListeners.remove(listener); + } + + private void singleScanInternal(boolean announceUnchangedDevices) { + try { + Delta delta = deltaUsbSerialScanner.scan(); + announceAddedDevices(delta.getAdded()); + announceRemovedDevices(delta.getRemoved()); + if (announceUnchangedDevices) { + announceAddedDevices(delta.getUnchanged()); + } + } catch (IOException e) { + logger.warn("A {} prevented a scan for USB serial devices: {}", e.getClass().getSimpleName(), + e.getMessage()); + } + } + + private void announceAddedDevices(Set deviceInfos) { + for (UsbSerialDeviceInformation deviceInfo : deviceInfos) { + for (UsbSerialDiscoveryListener listener : discoveryListeners) { + listener.usbSerialDeviceDiscovered(deviceInfo); + } + } + } + + private void announceRemovedDevices(Set 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