Skip to content

Commit

Permalink
Add-on suggestion finder for USB devices (#3922)
Browse files Browse the repository at this point in the history
Also-by: Holger Friedrich <[email protected]>
Signed-off-by: Andrew Fiddian-Green <[email protected]>
  • Loading branch information
andrewfg authored Dec 27, 2023
1 parent f5bd7f9 commit a93f3d7
Show file tree
Hide file tree
Showing 17 changed files with 451 additions and 12 deletions.
6 changes: 6 additions & 0 deletions bom/openhab-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,12 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.addon.usb</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.mdns</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public Set<AddonInfo> getSuggestedAddons() {
}

// now check if a process matches the pattern defined in addon.xml
logger.debug("Checking candidate: {}", candidate.getUID());
logger.trace("Checking candidate: {}", candidate.getUID());

for (AddonMatchProperty command : commands) {
logger.trace("Candidate {}, pattern \"{}\"", candidate.getUID(), command.getRegex());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ && propertyMatches(matchProperties, MODEL_URI, modelURI)
&& propertyMatches(matchProperties, SERIAL_NUMBER, serialNumber)
&& propertyMatches(matchProperties, FRIENDLY_NAME, friendlyName)) {
result.add(candidate);
logger.debug("Suggested addon found: {}", candidate.getUID());
logger.debug("Suggested add-on found: {}", candidate.getUID());
break;
}
}
Expand Down
29 changes: 29 additions & 0 deletions bundles/org.openhab.core.config.discovery.addon.usb/.classpath
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="annotationpath" value="target/dependency"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="annotationpath" value="target/dependency"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
23 changes: 23 additions & 0 deletions bundles/org.openhab.core.config.discovery.addon.usb/.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.config.discovery.addon.usb</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
14 changes: 14 additions & 0 deletions bundles/org.openhab.core.config.discovery.addon.usb/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
This content is produced and maintained by the openHAB project.

* Project home: https://www.openhab.org

== Declared Project Licenses

This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.

== Source Code

https://github.com/openhab/openhab-core

35 changes: 35 additions & 0 deletions bundles/org.openhab.core.config.discovery.addon.usb/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.reactor.bundles</artifactId>
<version>4.2.0-SNAPSHOT</version>
</parent>

<artifactId>org.openhab.core.config.discovery.addon.usb</artifactId>

<name>openHAB Core :: Bundles :: USB Suggested Add-on Finder</name>

<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.addon</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.addon</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.usbserial</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.config.discovery.addon.usb;

import static org.openhab.core.config.discovery.addon.AddonFinderConstants.SERVICE_NAME_USB;
import static org.openhab.core.config.discovery.addon.AddonFinderConstants.SERVICE_TYPE_USB;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.addon.AddonDiscoveryMethod;
import org.openhab.core.addon.AddonInfo;
import org.openhab.core.config.discovery.addon.AddonFinder;
import org.openhab.core.config.discovery.addon.BaseAddonFinder;
import org.openhab.core.config.discovery.usbserial.UsbSerialDeviceInformation;
import org.openhab.core.config.discovery.usbserial.UsbSerialDiscovery;
import org.openhab.core.config.discovery.usbserial.UsbSerialDiscoveryListener;
import org.osgi.service.component.annotations.Component;
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;

/**
* This is a {@link USBAddonFinder} for finding suggested add-ons related to USB devices.
* <p>
* It supports the following values for the 'match-property' 'name' element:
* <li>product - match on the product description text
* <li>manufacturer - match on the device manufacturer text
* <li>chipId - match on the chip vendor id plus product id
* <li>remote - match on whether the device is connected remotely or locally
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
@Component(service = AddonFinder.class, name = UsbAddonFinder.SERVICE_NAME)
public class UsbAddonFinder extends BaseAddonFinder implements UsbSerialDiscoveryListener {

public static final String SERVICE_TYPE = SERVICE_TYPE_USB;
public static final String SERVICE_NAME = SERVICE_NAME_USB;

/*
* Supported 'match-property' names
*/
public static final String PRODUCT = "product";
public static final String MANUFACTURER = "manufacturer";
public static final String CHIP_ID = "chipId";
public static final String REMOTE = "remote";

public static final Set<String> SUPPORTED_PROPERTIES = Set.of(PRODUCT, MANUFACTURER, CHIP_ID, REMOTE);

private final Logger logger = LoggerFactory.getLogger(UsbAddonFinder.class);
private final Set<UsbSerialDiscovery> usbSerialDiscoveries = new CopyOnWriteArraySet<>();
private final Map<Long, UsbSerialDeviceInformation> usbDeviceInformations = new ConcurrentHashMap<>();

@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void addUsbSerialDiscovery(UsbSerialDiscovery usbSerialDiscovery) {
usbSerialDiscoveries.add(usbSerialDiscovery);
usbSerialDiscovery.registerDiscoveryListener(this);
usbSerialDiscovery.doSingleScan();
}

protected synchronized void removeUsbSerialDiscovery(UsbSerialDiscovery usbSerialDiscovery) {
usbSerialDiscovery.unregisterDiscoveryListener(this);
usbSerialDiscoveries.remove(usbSerialDiscovery);
}

@Override
public Set<AddonInfo> getSuggestedAddons() {
Set<AddonInfo> result = new HashSet<>();
for (AddonInfo candidate : addonCandidates) {
for (AddonDiscoveryMethod method : candidate.getDiscoveryMethods().stream()
.filter(method -> SERVICE_TYPE.equals(method.getServiceType())).toList()) {
Map<String, Pattern> matchProperties = method.getMatchProperties().stream()
.collect(Collectors.toMap(property -> property.getName(), property -> property.getPattern()));

Set<String> propertyNames = new HashSet<>(matchProperties.keySet());
propertyNames.removeAll(SUPPORTED_PROPERTIES);

if (!propertyNames.isEmpty()) {
logger.warn("Add-on '{}' addon.xml file contains unsupported 'match-property' [{}]",
candidate.getUID(), String.join(",", propertyNames));
break;
}

logger.trace("Checking candidate: {}", candidate.getUID());
for (UsbSerialDeviceInformation device : usbDeviceInformations.values()) {
logger.trace("Checking device: {}", device);

if (propertyMatches(matchProperties, PRODUCT, device.getProduct())
&& propertyMatches(matchProperties, MANUFACTURER, device.getManufacturer())
&& propertyMatches(matchProperties, CHIP_ID,
getChipId(device.getVendorId(), device.getProductId()))
&& propertyMatches(matchProperties, REMOTE, String.valueOf(device.getRemote()))) {
result.add(candidate);
logger.debug("Suggested add-on found: {}", candidate.getUID());
break;
}
}
}
}
return result;
}

private String getChipId(int vendorId, int productId) {
return String.format("%04x:%04x", vendorId, productId);
}

@Override
public String getServiceName() {
return SERVICE_NAME;
}

/**
* Create a unique 33 bit integer map hash key comprising the remote flag in the upper bit, the vendorId in the
* middle 16 bits, and the productId in the lower 16 bits.
*/
private long keyOf(UsbSerialDeviceInformation deviceInfo) {
return (deviceInfo.getRemote() ? 0x1_0000_0000L : 0) + (deviceInfo.getVendorId() * 0x1_0000L)
+ deviceInfo.getProductId();
}

/**
* Add the discovered USB device information record to our internal map. If there is already an entry in the map
* then merge the two sets of data.
*
* @param discoveredInfo the newly discovered USB device information.
*/
@Override
public void usbSerialDeviceDiscovered(UsbSerialDeviceInformation discoveredInfo) {
UsbSerialDeviceInformation targetInfo = discoveredInfo;
UsbSerialDeviceInformation existingInfo = usbDeviceInformations.get(keyOf(targetInfo));

if (existingInfo != null) {
boolean isMerging = false;
String product = existingInfo.getProduct();
if (product != null) {
product = discoveredInfo.getProduct();
isMerging = true;
}
String manufacturer = existingInfo.getManufacturer();
if (manufacturer != null) {
manufacturer = discoveredInfo.getManufacturer();
isMerging = true;
}
String serialNumber = existingInfo.getSerialNumber();
if (serialNumber != null) {
serialNumber = discoveredInfo.getSerialNumber();
isMerging = true;
}
boolean remote = existingInfo.getRemote();
if (remote == discoveredInfo.getRemote()) {
isMerging = true;
}
if (isMerging) {
targetInfo = new UsbSerialDeviceInformation(discoveredInfo.getVendorId(), discoveredInfo.getProductId(),
serialNumber, manufacturer, product, discoveredInfo.getInterfaceNumber(),
discoveredInfo.getInterfaceDescription(), discoveredInfo.getSerialPort()).setRemote(remote);
}
}

usbDeviceInformations.put(keyOf(targetInfo), targetInfo);
}

@Override
public void usbSerialDeviceRemoved(UsbSerialDeviceInformation removedInfo) {
usbDeviceInformations.remove(keyOf(removedInfo));
}
}
Loading

0 comments on commit a93f3d7

Please sign in to comment.