New SDDP service for addon discovery and thing discovery (#4237)

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
Andrew Fiddian-Green 2024-05-30 20:03:58 +01:00 committed by GitHub
parent 9068ab2fac
commit c5336c5618
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1275 additions and 5 deletions

View File

@ -328,6 +328,12 @@
<version>${project.version}</version> <version>${project.version}</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.addon.sddp</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.core.bundles</groupId> <groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.addon.upnp</artifactId> <artifactId>org.openhab.core.config.discovery.addon.upnp</artifactId>
@ -346,6 +352,12 @@
<version>${project.version}</version> <version>${project.version}</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.sddp</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.core.bundles</groupId> <groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.usbserial</artifactId> <artifactId>org.openhab.core.config.discovery.usbserial</artifactId>

View File

@ -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>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.config.discovery.addon.upnp</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>

View File

@ -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

View File

@ -0,0 +1,34 @@
<?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.sddp</artifactId>
<name>openHAB Core :: Bundles :: SDDP 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.sddp</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,166 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.config.discovery.addon.sddp;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.AddonDiscoveryMethod;
import org.openhab.core.addon.AddonInfo;
import org.openhab.core.addon.AddonMatchProperty;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.config.discovery.addon.AddonFinder;
import org.openhab.core.config.discovery.addon.AddonFinderConstants;
import org.openhab.core.config.discovery.addon.BaseAddonFinder;
import org.openhab.core.config.discovery.sddp.SddpDevice;
import org.openhab.core.config.discovery.sddp.SddpDeviceParticipant;
import org.openhab.core.config.discovery.sddp.SddpDiscoveryService;
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.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a {@link SddpAddonFinder} for finding suggested Addons via SDDP.
* <p>
* It checks the binding's addon.xml 'match-property' elements for the following SDDP properties:
* <li>driver</li>
* <li>host</li>
* <li>ipAddress</li>
* <li>macAddress</li>
* <li>manufacturer</li>
* <li>model</li>
* <li>port</li>
* <li>primaryProxy</li>
* <li>proxies</li>
* <li>type</li>
* <p>
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
@Component(service = AddonFinder.class, name = SddpAddonFinder.SERVICE_NAME)
public class SddpAddonFinder extends BaseAddonFinder implements SddpDeviceParticipant {
public static final String SERVICE_TYPE = AddonFinderConstants.SERVICE_TYPE_SDDP;
public static final String SERVICE_NAME = AddonFinderConstants.SERVICE_NAME_SDDP;
private static final String DRIVER = "driver";
private static final String HOST = "host";
private static final String IP_ADDRESS = "ipAddress";
private static final String MAC_ADDRESS = "macAddress";
private static final String MANUFACTURER = "manufacturer";
private static final String MODEL = "model";
private static final String PORT = "port";
private static final String PRIMARY_PROXY = "primaryProxy";
private static final String PROXIES = "proxies";
private static final String TYPE = "type";
private static final Set<String> SUPPORTED_PROPERTIES = Set.of(DRIVER, HOST, IP_ADDRESS, MAC_ADDRESS, MANUFACTURER,
MODEL, PORT, PRIMARY_PROXY, PROXIES, TYPE);
private final Logger logger = LoggerFactory.getLogger(SddpAddonFinder.class);
private final Set<SddpDevice> foundDevices = new HashSet<>();
private @Nullable SddpDiscoveryService sddpDiscoveryService = null;
@Activate
public SddpAddonFinder(
@Reference(service = DiscoveryService.class, target = "(protocol=sddp)") DiscoveryService discoveryService) {
if (discoveryService instanceof SddpDiscoveryService sddpDiscoveryService) {
sddpDiscoveryService.addSddpDeviceParticipant(this);
this.sddpDiscoveryService = sddpDiscoveryService;
} else {
logger.warn("SddpAddonFinder() DiscoveryService is not an SddpDiscoveryService");
}
}
@Deactivate
public void deactivate() {
SddpDiscoveryService sddpDiscoveryService = this.sddpDiscoveryService;
if (sddpDiscoveryService != null) {
sddpDiscoveryService.removeSddpDeviceParticipant(this);
this.sddpDiscoveryService = null;
}
unsetAddonCandidates();
foundDevices.clear();
}
@Override
public void deviceAdded(SddpDevice device) {
foundDevices.add(device);
}
@Override
public void deviceRemoved(SddpDevice device) {
foundDevices.remove(device);
}
@Override
public String getServiceName() {
return SERVICE_NAME;
}
@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(AddonMatchProperty::getName, AddonMatchProperty::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 (SddpDevice device : foundDevices) {
logger.trace("Checking device: {}", device.host);
if (propertyMatches(matchProperties, HOST, device.host)
&& propertyMatches(matchProperties, IP_ADDRESS, device.ipAddress)
&& propertyMatches(matchProperties, MAC_ADDRESS, device.macAddress)
&& propertyMatches(matchProperties, MANUFACTURER, device.manufacturer)
&& propertyMatches(matchProperties, MODEL, device.model)
&& propertyMatches(matchProperties, PORT, device.port)
&& propertyMatches(matchProperties, PRIMARY_PROXY, device.primaryProxy)
&& propertyMatches(matchProperties, PROXIES, device.proxies)
&& propertyMatches(matchProperties, TYPE, device.type)) {
result.add(candidate);
logger.debug("Suggested add-on found: {}", candidate.getUID());
break;
}
}
}
}
return result;
}
@Override
public void setAddonCandidates(List<AddonInfo> candidates) {
super.setAddonCandidates(candidates);
}
}

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.config.discovery.addon.sddp.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.core.addon.AddonDiscoveryMethod;
import org.openhab.core.addon.AddonInfo;
import org.openhab.core.addon.AddonMatchProperty;
import org.openhab.core.config.discovery.addon.sddp.SddpAddonFinder;
import org.openhab.core.config.discovery.sddp.SddpDevice;
import org.openhab.core.config.discovery.sddp.SddpDiscoveryService;
/**
* JUnit tests for the {@link SddpAddonFinder}.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class SddpAddonFinderTests {
private static final Map<String, String> DEVICE_FIELDS = Map.of(
// @formatter:off
"From", "\"192.168.4.237:1902\"",
"Host", "\"JVC_PROJECTOR-E0DADC152802\"",
"Max-Age", "1800",
"Type", "\"JVCKENWOOD:Projector\"",
"Primary-Proxy", "\"projector\"",
"Proxies", "\"projector\"",
"Manufacturer", "\"JVCKENWOOD\"",
"Model", "\"DLA-RS3100_NZ8\"",
"Driver", "\"projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i\"");
// @formatter:on
private List<AddonInfo> createAddonInfos() {
AddonDiscoveryMethod method = new AddonDiscoveryMethod().setServiceType(SddpAddonFinder.SERVICE_TYPE)
.setMatchProperties(List.of(new AddonMatchProperty("host", "JVC.*")));
List<AddonInfo> addonInfos = new ArrayList<>();
addonInfos.add(AddonInfo.builder("jvc", "binding").withName("JVC").withDescription("JVC Kenwood")
.withDiscoveryMethods(List.of(method)).build());
return addonInfos;
}
@Test
public void testFinder() {
SddpDevice device = new SddpDevice(DEVICE_FIELDS, false);
List<AddonInfo> addonInfos = createAddonInfos();
SddpAddonFinder finder = new SddpAddonFinder(mock(SddpDiscoveryService.class));
finder.setAddonCandidates(addonInfos);
Set<AddonInfo> suggestions;
AddonInfo info;
finder.deviceAdded(device);
suggestions = finder.getSuggestedAddons();
assertFalse(suggestions.isEmpty());
info = suggestions.stream().findFirst().orElse(null);
assertNotNull(info);
assertEquals("JVC Kenwood", info.getDescription());
finder.deviceRemoved(device);
suggestions = finder.getSuggestedAddons();
assertTrue(suggestions.isEmpty());
}
}

View File

@ -35,6 +35,10 @@ public class AddonFinderConstants {
public static final String CFG_FINDER_MDNS = "suggestionFinderMdns"; public static final String CFG_FINDER_MDNS = "suggestionFinderMdns";
public static final String SERVICE_NAME_MDNS = SERVICE_TYPE_MDNS + ADDON_SUGGESTION_FINDER; public static final String SERVICE_NAME_MDNS = SERVICE_TYPE_MDNS + ADDON_SUGGESTION_FINDER;
public static final String SERVICE_TYPE_SDDP = "sddp";
public static final String CFG_FINDER_SDDP = "suggestionFinderSddp";
public static final String SERVICE_NAME_SDDP = SERVICE_TYPE_SDDP + ADDON_SUGGESTION_FINDER;
public static final String SERVICE_TYPE_UPNP = "upnp"; public static final String SERVICE_TYPE_UPNP = "upnp";
public static final String CFG_FINDER_UPNP = "suggestionFinderUpnp"; public static final String CFG_FINDER_UPNP = "suggestionFinderUpnp";
public static final String SERVICE_NAME_UPNP = SERVICE_TYPE_UPNP + ADDON_SUGGESTION_FINDER; public static final String SERVICE_NAME_UPNP = SERVICE_TYPE_UPNP + ADDON_SUGGESTION_FINDER;
@ -43,13 +47,14 @@ public class AddonFinderConstants {
public static final String CFG_FINDER_USB = "suggestionFinderUsb"; public static final String CFG_FINDER_USB = "suggestionFinderUsb";
public static final String SERVICE_NAME_USB = SERVICE_TYPE_USB + ADDON_SUGGESTION_FINDER; public static final String SERVICE_NAME_USB = SERVICE_TYPE_USB + ADDON_SUGGESTION_FINDER;
public static final List<String> SUGGESTION_FINDERS = List.of(SERVICE_NAME_IP, SERVICE_NAME_MDNS, SERVICE_NAME_UPNP, public static final List<String> SUGGESTION_FINDERS = List.of(SERVICE_NAME_IP, SERVICE_NAME_MDNS, SERVICE_NAME_SDDP,
SERVICE_NAME_USB); SERVICE_NAME_UPNP, SERVICE_NAME_USB);
public static final Map<String, String> SUGGESTION_FINDER_TYPES = Map.of(SERVICE_NAME_IP, SERVICE_TYPE_IP, public static final Map<String, String> SUGGESTION_FINDER_TYPES = Map.of(SERVICE_NAME_IP, SERVICE_TYPE_IP,
SERVICE_NAME_MDNS, SERVICE_TYPE_MDNS, SERVICE_NAME_UPNP, SERVICE_TYPE_UPNP, SERVICE_NAME_USB, SERVICE_NAME_MDNS, SERVICE_TYPE_MDNS, SERVICE_NAME_SDDP, SERVICE_TYPE_SDDP, SERVICE_NAME_UPNP,
SERVICE_TYPE_USB); SERVICE_TYPE_UPNP, SERVICE_NAME_USB, SERVICE_TYPE_USB);
public static final Map<String, String> SUGGESTION_FINDER_CONFIGS = Map.of(SERVICE_NAME_IP, CFG_FINDER_IP, public static final Map<String, String> SUGGESTION_FINDER_CONFIGS = Map.of(SERVICE_NAME_IP, CFG_FINDER_IP,
SERVICE_NAME_MDNS, CFG_FINDER_MDNS, SERVICE_NAME_UPNP, CFG_FINDER_UPNP, SERVICE_NAME_USB, CFG_FINDER_USB); SERVICE_NAME_MDNS, CFG_FINDER_MDNS, SERVICE_NAME_SDDP, CFG_FINDER_SDDP, SERVICE_NAME_UPNP, CFG_FINDER_UPNP,
SERVICE_NAME_USB, CFG_FINDER_USB);
} }

View File

@ -0,0 +1,34 @@
<?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="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="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4">
<attributes>
<attribute name="annotationpath" value="target/dependency"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.config.discovery.upnp</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.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -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

View File

@ -0,0 +1,25 @@
<?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.sddp</artifactId>
<name>openHAB Core :: Bundles :: Configuration SDDP Discovery</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.config.discovery.sddp;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* A DTO class containing data from an SDDP device discovery result.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class SddpDevice {
public final String from;
public final String host;
public final String maxAge;
public final String type;
public final String primaryProxy;
public final String proxies;
public final String manufacturer;
public final String model;
public final String driver;
public final String ipAddress;
public final String port;
public final String macAddress;
public final Instant expireInstant;
/**
* Constructor.
*
* @param headers a map of parameter name / value pairs.
* @param offline indicates if the device is being created from a NOTIFY OFFLINE announcement.
*/
public SddpDevice(Map<String, String> headers, boolean offline) {
from = headers.getOrDefault("From", "").replaceAll("^\"|\"$", "");
host = headers.getOrDefault("Host", "").replaceAll("^\"|\"$", "");
maxAge = headers.getOrDefault("Max-Age", "").replaceAll("^\"|\"$", "");
type = headers.getOrDefault("Type", "").replaceAll("^\"|\"$", "");
primaryProxy = headers.getOrDefault("Primary-Proxy", "").replaceAll("^\"|\"$", "");
proxies = headers.getOrDefault("Proxies", "").replaceAll("^\"|\"$", "");
manufacturer = headers.getOrDefault("Manufacturer", "").replaceAll("^\"|\"$", "");
model = headers.getOrDefault("Model", "").replaceAll("^\"|\"$", "");
driver = headers.getOrDefault("Driver", "").replaceAll("^\"|\"$", "");
String[] fromParts = from.split(":");
ipAddress = fromParts[0];
port = fromParts.length > 1 ? fromParts[1] : "";
String[] hostParts = host.split("-|_");
macAddress = hostParts.length <= 1 ? ""
: hostParts[hostParts.length - 1].replaceAll("(..)(?!$)", "$1-").toLowerCase();
expireInstant = offline ? Instant.now().minusMillis(1)
: Instant.now().plusSeconds(maxAge.isBlank() ? 0 : Integer.parseInt(maxAge));
}
/**
* Set uniqueness is determined by the From field only
*/
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof SddpDevice other) {
return Objects.equals(from, other.from);
}
return false;
}
/**
* Set uniqueness is determined by the From field only
*/
@Override
public int hashCode() {
return Objects.hash(from);
}
/**
* Check if the creation time plus max-age instant is exceeded.
*/
public boolean isExpired() {
return Instant.now().isAfter(expireInstant);
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.config.discovery.sddp;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* A {@link SddpDeviceParticipant} that is registered as a service is picked up by the {@link SddpDiscoveryService} and
* can thus be informed when the SDDP service discovers or removes an {@link SddpDevice}.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public interface SddpDeviceParticipant {
void deviceAdded(SddpDevice device);
void deviceRemoved(SddpDevice device);
}

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.config.discovery.sddp;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
/**
* A {@link SddpDiscoveryParticipant} that is registered as a service is picked up by the {@link SddpDiscoveryService}
* and can thus contribute {@link DiscoveryResult}s from SDDP scans.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public interface SddpDiscoveryParticipant {
/**
* Defines the list of thing types that this participant can identify
*
* @return a set of thing type UIDs for which results can be created
*/
Set<ThingTypeUID> getSupportedThingTypeUIDs();
/**
* Creates a discovery result for a SDDP device
*
* @param device the SDDP device found on the network
* @return the according discovery result or <code>null</code>, if device is not
* supported by this participant
*/
@Nullable
DiscoveryResult createResult(SddpDevice device);
/**
* Returns the thing UID for a SDDP device
*
* @param device the SDDP device on the network
* @return a thing UID or <code>null</code>, if device is not supported
* by this participant
*/
@Nullable
ThingUID getThingUID(SddpDevice device);
}

View File

@ -0,0 +1,438 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.config.discovery.sddp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.SocketTimeoutException;
import java.net.StandardSocketOptions;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.net.CidrAddress;
import org.openhab.core.net.NetworkAddressChangeListener;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.framework.FrameworkUtil;
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;
/**
* This is a {@link DiscoveryService} implementation, which can find SDDP devices in the network.
* Support for bindings can be achieved by implementing and registering a {@link SddpDiscoveryParticipant}.
* Support for finders can be achieved by implementing and registering a {@link SddpDeviceParticipant}.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = DiscoveryService.class, property = "protocol=sddp", configurationPid = "discovery.sddp")
public class SddpDiscoveryService extends AbstractDiscoveryService
implements AutoCloseable, NetworkAddressChangeListener {
private static final int SDDP_PORT = 1902;
private static final String SDDP_IP_ADDRESS = "239.255.255.250";
private static final InetSocketAddress SDDP_GROUP = new InetSocketAddress(SDDP_IP_ADDRESS, SDDP_PORT);
private static final int READ_BUFFER_SIZE = 1024;
private static final Duration SOCKET_TIMOUT = Duration.ofMillis(1000);
private static final Duration SEARCH_LISTEN_DURATION = Duration.ofSeconds(5);
private static final Duration CACHE_PURGE_INTERVAL = Duration.ofSeconds(300);
private static final String SEARCH_REQUEST_BODY_FORMAT = "SEARCH * SDDP/1.0\r\nHost: \"%s:%d\"\r\n";
private static final String SEARCH_RESPONSE_HEADER = "SDDP/1.0 200 OK";
private static final String NOTIFY_ALIVE_HEADER = "NOTIFY ALIVE SDDP/1.0";
private static final String NOTIFY_OFFLINE_HEADER = "NOTIFY OFFLINE SDDP/1.0";
private final Logger logger = LoggerFactory.getLogger(SddpDiscoveryService.class);
private final Set<SddpDevice> foundDevicesCache = ConcurrentHashMap.newKeySet();
private final Set<SddpDiscoveryParticipant> discoveryParticipants = ConcurrentHashMap.newKeySet();
private final Set<SddpDeviceParticipant> deviceParticipants = ConcurrentHashMap.newKeySet();
private final NetworkAddressService networkAddressService;
private boolean closing = false;
private @Nullable Future<?> listenBackgroundMulticastTask = null;
private @Nullable Future<?> listenActiveScanUnicastTask = null;
private @Nullable ScheduledFuture<?> purgeExpiredDevicesTask = null;
@Activate
public SddpDiscoveryService(final @Nullable Map<String, Object> configProperties, //
final @Reference NetworkAddressService networkAddressService, //
final @Reference TranslationProvider i18nProvider, //
final @Reference LocaleProvider localeProvider) {
super((int) SEARCH_LISTEN_DURATION.getSeconds());
this.networkAddressService = networkAddressService;
this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider;
super.activate(configProperties); // note: this starts listenBackgroundMulticastTask
purgeExpiredDevicesTask = scheduler.scheduleWithFixedDelay(() -> purgeExpiredDevices(),
CACHE_PURGE_INTERVAL.getSeconds(), CACHE_PURGE_INTERVAL.getSeconds(), TimeUnit.SECONDS);
}
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
public void addSddpDeviceParticipant(SddpDeviceParticipant participant) {
deviceParticipants.add(participant);
foundDevicesCache.stream().filter(d -> !d.isExpired()).forEach(d -> participant.deviceAdded(d));
startScan();
}
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void addSddpDiscoveryParticipant(SddpDiscoveryParticipant participant) {
discoveryParticipants.add(participant);
foundDevicesCache.stream().filter(d -> !d.isExpired()).forEach(d -> {
DiscoveryResult result = participant.createResult(d);
if (result != null) {
DiscoveryResult localizedResult = getLocalizedDiscoveryResult(result,
FrameworkUtil.getBundle(participant.getClass()));
thingDiscovered(localizedResult);
}
});
}
/**
* Cancel the given task.
*/
private void cancelTask(@Nullable Future<?> task) {
if (task != null) {
task.cancel(true);
}
}
@Override
public void close() {
deactivate();
}
/**
* Optionally create an {@link SddpDevice) object from UDP packet data if the data is good.
*/
public Optional<SddpDevice> createSddpDevice(String data) {
if (!data.isBlank()) {
List<String> lines = data.lines().toList();
if (lines.size() > 1) {
String statement = lines.get(0).strip();
boolean offline = statement.startsWith(NOTIFY_OFFLINE_HEADER);
if (offline || statement.startsWith(NOTIFY_ALIVE_HEADER)
|| statement.startsWith(SEARCH_RESPONSE_HEADER)) {
Map<String, String> headers = new HashMap<>();
for (int i = 1; i < lines.size(); i++) {
String[] header = lines.get(i).split(":", 2);
if (header.length > 1) {
headers.put(header[0].strip(), header[1].strip());
}
}
return Optional.of(new SddpDevice(headers, offline));
}
}
}
return Optional.empty();
}
@Deactivate
@Override
protected void deactivate() {
closing = true;
foundDevicesCache.clear();
discoveryParticipants.clear();
deviceParticipants.clear();
super.deactivate(); // note: this cancels and nulls listenBackgroundMulticastTask
cancelTask(listenActiveScanUnicastTask);
listenActiveScanUnicastTask = null;
cancelTask(purgeExpiredDevicesTask);
purgeExpiredDevicesTask = null;
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
Set<ThingTypeUID> supportedThingTypes = new HashSet<>();
discoveryParticipants.forEach(p -> supportedThingTypes.addAll(p.getSupportedThingTypeUIDs()));
return supportedThingTypes;
}
/**
* Continue to listen for incoming SDDP multicast messages until the thread is externally interrupted.
*/
private void listenBackGroundMulticast() {
MulticastSocket socket = null;
NetworkInterface networkInterface = null;
try {
networkInterface = NetworkInterface
.getByInetAddress(InetAddress.getByName(networkAddressService.getPrimaryIpv4HostAddress()));
if (logger.isDebugEnabled()) {
logger.debug("listenBackGroundMulticast() starting on interface '{}'",
networkInterface.getDisplayName());
}
socket = new MulticastSocket(SDDP_PORT);
socket.joinGroup(SDDP_GROUP, networkInterface);
socket.setSoTimeout((int) SOCKET_TIMOUT.toMillis());
DatagramPacket packet = null;
byte[] buffer = new byte[READ_BUFFER_SIZE];
// loop listen for responses
while (!Thread.currentThread().isInterrupted()) {
try {
if (packet == null) {
packet = new DatagramPacket(buffer, buffer.length);
}
socket.receive(packet);
processPacket(packet);
packet = null;
} catch (SocketTimeoutException e) {
// socket.receive() will time out every 1 second so the thread won't block
}
}
} catch (IOException e) {
if (!closing) {
logger.warn("listenBackGroundMulticast error '{}'", e.getMessage());
}
} finally {
if (socket != null && networkInterface != null) {
try {
socket.leaveGroup(SDDP_GROUP, networkInterface);
} catch (IOException e) {
if (!closing) {
logger.warn("listenBackGroundMulticast() error '{}'", e.getMessage());
}
}
socket.close();
}
}
}
/**
* Send a single outgoing SEARCH 'ping' and then continue to listen for incoming SDDP unicast responses until the
* loop time elapses or the thread is externally interrupted.
*/
private void listenActiveScanUnicast() {
// get a free port number
int port;
try (ServerSocket portFinder = new ServerSocket(0)) {
port = portFinder.getLocalPort();
} catch (IOException e) {
logger.warn("listenActiveScanUnicast() port finder error '{}'", e.getMessage());
return;
}
try (DatagramSocket socket = new DatagramSocket(port)) {
String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
NetworkInterface networkInterface = NetworkInterface.getByInetAddress(InetAddress.getByName(ipAddress));
if (logger.isDebugEnabled()) {
logger.debug("listenActiveScanUnicast() starting on '{}:{}' on interface '{}'", ipAddress, port,
networkInterface.getDisplayName());
}
socket.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);
socket.setSoTimeout((int) SOCKET_TIMOUT.toMillis());
DatagramPacket packet;
byte[] buffer;
// send search request
String search = String.format(SEARCH_REQUEST_BODY_FORMAT, ipAddress, port);
buffer = search.getBytes(StandardCharsets.UTF_8);
packet = new DatagramPacket(buffer, buffer.length, new InetSocketAddress(SDDP_IP_ADDRESS, SDDP_PORT));
socket.send(packet);
logger.trace("Packet sent to '{}:{}' content:\r\n{}", SDDP_IP_ADDRESS, SDDP_PORT, search);
final Instant listenDoneTime = Instant.now().plus(SEARCH_LISTEN_DURATION);
buffer = new byte[READ_BUFFER_SIZE];
packet = null;
// loop listen for responses
while (Instant.now().isBefore(listenDoneTime) && !Thread.currentThread().isInterrupted()) {
try {
if (packet == null) {
packet = new DatagramPacket(buffer, buffer.length);
}
socket.receive(packet);
processPacket(packet);
packet = null;
} catch (SocketTimeoutException e) {
// receive will time out every 1 second so the thread won't block
}
}
} catch (IOException e) {
if (!closing) {
logger.warn("listenActiveScanUnicast() error '{}'", e.getMessage());
}
}
}
@Modified
@Override
protected void modified(@Nullable Map<String, Object> configProperties) {
super.modified(configProperties);
}
/**
* If the network interfaces change then cancel and recreate all pending tasks.
*/
@Override
public synchronized void onChanged(List<CidrAddress> added, List<CidrAddress> removed) {
Future<?> multicastTask = listenBackgroundMulticastTask;
if (multicastTask != null && !multicastTask.isDone()) {
multicastTask.cancel(true);
listenBackgroundMulticastTask = scheduler.submit(() -> listenBackGroundMulticast());
}
Future<?> unicastTask = listenActiveScanUnicastTask;
if (unicastTask != null && !unicastTask.isDone()) {
unicastTask.cancel(true);
listenActiveScanUnicastTask = scheduler.submit(() -> listenActiveScanUnicast());
}
}
/**
* Process the {@link DatagramPacket} content by trying to create an {@link SddpDevice} and eventually adding it to
* the foundDevicesCache, and if so, then notifying all listeners.
*
* @param packet a datagram packet that arrived over the network.
*/
private synchronized void processPacket(DatagramPacket packet) {
String content = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
if (logger.isTraceEnabled()) {
logger.trace("Packet received from '{}:{}' content:\r\n{}", packet.getAddress().getHostAddress(),
packet.getPort(), content);
}
Optional<SddpDevice> deviceOptional = createSddpDevice(content);
if (deviceOptional.isPresent()) {
SddpDevice device = deviceOptional.get();
foundDevicesCache.remove(device);
if (device.isExpired()) {
// device created from a NOTIFY OFFLINE announcement
discoveryParticipants.forEach(p -> {
DiscoveryResult discoveryResult = p.createResult(device);
if (discoveryResult != null) {
thingRemoved(discoveryResult.getThingUID());
}
});
deviceParticipants.forEach(f -> f.deviceRemoved(device));
} else {
// device created from a NOTIFY ALIVE announcement or SEARCH response
foundDevicesCache.add(device);
discoveryParticipants.forEach(p -> {
DiscoveryResult discoveryResult = p.createResult(device);
if (discoveryResult != null) {
DiscoveryResult localizedResult = getLocalizedDiscoveryResult(discoveryResult,
FrameworkUtil.getBundle(p.getClass()));
thingDiscovered(localizedResult);
}
});
deviceParticipants.forEach(f -> f.deviceAdded(device));
}
if (logger.isDebugEnabled()) {
logger.debug("processPacket() foundDevices={}, deviceParticipants={}, discoveryParticipants={}",
foundDevicesCache.size(), deviceParticipants.size(), discoveryParticipants.size());
}
}
}
/**
* Purge expired devices and notify all listeners.
*/
private synchronized void purgeExpiredDevices() {
Set<SddpDevice> devices = new HashSet<>(foundDevicesCache);
devices.stream().filter(d -> d.isExpired()).forEach(d -> {
discoveryParticipants.forEach(p -> {
ThingUID thingUID = p.getThingUID(d);
if (thingUID != null) {
thingRemoved(thingUID);
}
});
deviceParticipants.forEach(f -> f.deviceRemoved(d));
});
foundDevicesCache.clear();
foundDevicesCache.addAll(devices.stream().filter(d -> !d.isExpired()).collect(Collectors.toSet()));
}
public void removeSddpDeviceParticipant(SddpDeviceParticipant participant) {
deviceParticipants.remove(participant);
}
public void removeSddpDiscoveryParticipant(SddpDiscoveryParticipant participant) {
discoveryParticipants.remove(participant);
}
@Override
protected void startBackgroundDiscovery() {
Future<?> task = listenBackgroundMulticastTask;
if (task == null || task.isDone()) {
listenBackgroundMulticastTask = scheduler.submit(() -> listenBackGroundMulticast());
}
}
/**
* Schedule to send one single SDDP SEARCH request, and listen for responses.
*/
@Override
protected void startScan() {
Future<?> task = listenActiveScanUnicastTask;
if (task == null || task.isDone()) {
listenActiveScanUnicastTask = scheduler.submit(() -> listenActiveScanUnicast());
}
}
@Override
protected void stopBackgroundDiscovery() {
cancelTask(listenBackgroundMulticastTask);
listenBackgroundMulticastTask = null;
}
}

View File

@ -0,0 +1,158 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.config.discovery.sddp.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.openhab.core.config.discovery.sddp.SddpDevice;
import org.openhab.core.config.discovery.sddp.SddpDiscoveryService;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.net.NetworkAddressService;
/**
* JUnit tests for parsing SDDP discovery results.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
@TestInstance(Lifecycle.PER_CLASS)
public class SddpDiscoveryServiceTests {
private static final String ALIVE_NOTIFICATION = """
NOTIFY ALIVE SDDP/1.0
From: "192.168.4.237:1902"
Host: "JVC_PROJECTOR-E0DADC152802"
Max-Age: 1800
Type: "JVCKENWOOD:Projector"
Primary-Proxy: "projector"
Proxies: "projector"
Manufacturer: "JVCKENWOOD"
Model: "DLA-RS3100_NZ8"
Driver: "projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i"
""";
private static final String BAD_HEADER = """
SDDP/1.0 404 NOT FOUND\r
From: "192.168.4.237:1902"\r
Host: "JVC_PROJECTOR-E0DADC152802"\r
Max-Age: 1800\r
Type: "JVCKENWOOD:Projector"\r
Primary-Proxy: "projector"\r
Proxies: "projector"\r
Manufacturer: "JVCKENWOOD"\r
Model: "DLA-RS3100_NZ8"\r
Driver: "projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i"\r
""";
private static final String BAD_PAYLOAD = """
SDDP/1.0 200 OK\r
""";
private static final String SEARCH_RESPONSE = """
SDDP/1.0 200 OK\r
From: "192.168.4.237:1902"\r
Host: "JVC_PROJECTOR-E0DADC152802"\r
Max-Age: 1800\r
Type: "JVCKENWOOD:Projector"\r
Primary-Proxy: "projector"\r
Proxies: "projector"\r
Manufacturer: "JVCKENWOOD"\r
Model: "DLA-RS3100_NZ8"\r
Driver: "projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i"\r
""";
private @NonNullByDefault({}) NetworkAddressService networkAddressService;
@BeforeAll
public void setup() {
networkAddressService = mock(NetworkAddressService.class);
when(networkAddressService.getPrimaryIpv4HostAddress()).thenReturn("192.168.1.1");
}
@Test
void testAliveNotification() throws Exception {
try (SddpDiscoveryService service = new SddpDiscoveryService(null, networkAddressService,
mock(TranslationProvider.class), mock(LocaleProvider.class))) {
Optional<SddpDevice> deviceOptional = service.createSddpDevice(ALIVE_NOTIFICATION);
assertTrue(deviceOptional.isPresent());
SddpDevice device = deviceOptional.orElse(null);
assertNotNull(device);
assertEquals("192.168.4.237:1902", device.from);
assertEquals("JVC_PROJECTOR-E0DADC152802", device.host);
assertEquals("1800", device.maxAge);
assertEquals("JVCKENWOOD:Projector", device.type);
assertEquals("projector", device.primaryProxy);
assertEquals("projector", device.proxies);
assertEquals("JVCKENWOOD", device.manufacturer);
assertEquals("DLA-RS3100_NZ8", device.model);
assertEquals("projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i", device.driver);
assertEquals("192.168.4.237", device.ipAddress);
assertEquals("e0-da-dc-15-28-02", device.macAddress);
assertEquals("1902", device.port);
}
}
@Test
void testBadHeader() throws Exception {
try (SddpDiscoveryService service = new SddpDiscoveryService(null, networkAddressService,
mock(TranslationProvider.class), mock(LocaleProvider.class))) {
Optional<SddpDevice> deviceOptional = service.createSddpDevice(BAD_HEADER);
assertFalse(deviceOptional.isPresent());
}
}
@Test
void testBadPayload() throws Exception {
try (SddpDiscoveryService service = new SddpDiscoveryService(null, networkAddressService,
mock(TranslationProvider.class), mock(LocaleProvider.class))) {
Optional<SddpDevice> deviceOptional = service.createSddpDevice(BAD_PAYLOAD);
assertFalse(deviceOptional.isPresent());
}
}
@Test
void testSearchResponse() throws Exception {
try (SddpDiscoveryService service = new SddpDiscoveryService(null, networkAddressService,
mock(TranslationProvider.class), mock(LocaleProvider.class))) {
Optional<SddpDevice> deviceOptional = service.createSddpDevice(SEARCH_RESPONSE);
assertTrue(deviceOptional.isPresent());
SddpDevice device = deviceOptional.orElse(null);
assertNotNull(device);
assertEquals("192.168.4.237:1902", device.from);
assertEquals("JVC_PROJECTOR-E0DADC152802", device.host);
assertEquals("1800", device.maxAge);
assertEquals("JVCKENWOOD:Projector", device.type);
assertEquals("projector", device.primaryProxy);
assertEquals("projector", device.proxies);
assertEquals("JVCKENWOOD", device.manufacturer);
assertEquals("DLA-RS3100_NZ8", device.model);
assertEquals("projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i", device.driver);
assertEquals("192.168.4.237", device.ipAddress);
assertEquals("e0-da-dc-15-28-02", device.macAddress);
assertEquals("1902", device.port);
}
}
}

View File

@ -34,6 +34,12 @@
<label>IP-based Suggestion Finder</label> <label>IP-based Suggestion Finder</label>
<description>Use IP network discovery broadcasts to suggest add-ons. Enabling/disabling may take up to 1 minute.</description> <description>Use IP network discovery broadcasts to suggest add-ons. Enabling/disabling may take up to 1 minute.</description>
</parameter> </parameter>
<parameter name="suggestionFinderSddp" type="boolean">
<advanced>true</advanced>
<label>SDDP Suggestion Finder</label>
<description>Use SDDP network scan to suggest add-ons. Enabling/disabling may take up to 1 minute.</description>
<default>true</default>
</parameter>
<parameter name="suggestionFinderUsb" type="boolean"> <parameter name="suggestionFinderUsb" type="boolean">
<advanced>true</advanced> <advanced>true</advanced>
<label>USB Suggestion Finder</label> <label>USB Suggestion Finder</label>

View File

@ -6,6 +6,8 @@ system.config.addons.suggestionFinderIp.label = IP-based Suggestion Finder
system.config.addons.suggestionFinderIp.description = Use IP network discovery broadcasts to suggest add-ons. Enabling/disabling may take up to 1 minute. system.config.addons.suggestionFinderIp.description = Use IP network discovery broadcasts to suggest add-ons. Enabling/disabling may take up to 1 minute.
system.config.addons.suggestionFinderMdns.label = mDNS Suggestion Finder system.config.addons.suggestionFinderMdns.label = mDNS Suggestion Finder
system.config.addons.suggestionFinderMdns.description = Use mDNS network scan to suggest add-ons. Enabling/disabling may take up to 1 minute. system.config.addons.suggestionFinderMdns.description = Use mDNS network scan to suggest add-ons. Enabling/disabling may take up to 1 minute.
system.config.addons.suggestionFinderSddp.label = SDDP Suggestion Finder
system.config.addons.suggestionFinderSddp.description = Use SDDP network scan to suggest add-ons. Enabling/disabling may take up to 1 minute.
system.config.addons.suggestionFinderUpnp.label = UPnP Suggestion Finder system.config.addons.suggestionFinderUpnp.label = UPnP Suggestion Finder
system.config.addons.suggestionFinderUpnp.description = Use UPnP network scan to suggest add-ons. Enabling/disabling may take up to 1 minute. system.config.addons.suggestionFinderUpnp.description = Use UPnP network scan to suggest add-ons. Enabling/disabling may take up to 1 minute.
system.config.addons.suggestionFinderUsb.label = USB Suggestion Finder system.config.addons.suggestionFinderUsb.label = USB Suggestion Finder

View File

@ -34,9 +34,11 @@
<module>org.openhab.core.config.discovery.addon.ip</module> <module>org.openhab.core.config.discovery.addon.ip</module>
<module>org.openhab.core.config.discovery.addon.mdns</module> <module>org.openhab.core.config.discovery.addon.mdns</module>
<module>org.openhab.core.config.discovery.addon.process</module> <module>org.openhab.core.config.discovery.addon.process</module>
<module>org.openhab.core.config.discovery.addon.sddp</module>
<module>org.openhab.core.config.discovery.addon.upnp</module> <module>org.openhab.core.config.discovery.addon.upnp</module>
<module>org.openhab.core.config.discovery.addon.usb</module> <module>org.openhab.core.config.discovery.addon.usb</module>
<module>org.openhab.core.config.discovery.mdns</module> <module>org.openhab.core.config.discovery.mdns</module>
<module>org.openhab.core.config.discovery.sddp</module>
<module>org.openhab.core.config.discovery.usbserial</module> <module>org.openhab.core.config.discovery.usbserial</module>
<module>org.openhab.core.config.discovery.usbserial.linuxsysfs</module> <module>org.openhab.core.config.discovery.usbserial.linuxsysfs</module>
<module>org.openhab.core.config.discovery.usbserial.ser2net</module> <module>org.openhab.core.config.discovery.usbserial.ser2net</module>

View File

@ -105,6 +105,18 @@
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon.usb/${project.version}</bundle> <bundle>mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon.usb/${project.version}</bundle>
</feature> </feature>
<feature name="openhab-core-config-discovery-addon-sddp" version="${project.version}">
<feature>openhab-core-base</feature>
<feature>openhab-core-config-discovery-addon</feature>
<feature>openhab-core-config-discovery-sddp</feature>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon.sddp/${project.version}</bundle>
</feature>
<feature name="openhab-core-config-discovery-sddp" version="${project.version}">
<feature>openhab-core-base</feature>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.sddp/${project.version}</bundle>
</feature>
<feature name="openhab-core-addon-marketplace" version="${project.version}"> <feature name="openhab-core-addon-marketplace" version="${project.version}">
<feature>kar</feature> <feature>kar</feature>
<feature>openhab-core-base</feature> <feature>openhab-core-base</feature>