mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-10 13:21:53 +01:00
New SDDP service for addon discovery and thing discovery (#4237)
Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
parent
9068ab2fac
commit
c5336c5618
@ -328,6 +328,12 @@
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</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>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.config.discovery.addon.upnp</artifactId>
|
||||
@ -346,6 +352,12 @@
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</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>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.config.discovery.usbserial</artifactId>
|
||||
|
@ -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>
|
@ -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>
|
14
bundles/org.openhab.core.config.discovery.addon.sddp/NOTICE
Normal file
14
bundles/org.openhab.core.config.discovery.addon.sddp/NOTICE
Normal 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
|
||||
|
34
bundles/org.openhab.core.config.discovery.addon.sddp/pom.xml
Normal file
34
bundles/org.openhab.core.config.discovery.addon.sddp/pom.xml
Normal 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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -35,6 +35,10 @@ public class AddonFinderConstants {
|
||||
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_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 CFG_FINDER_UPNP = "suggestionFinderUpnp";
|
||||
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 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,
|
||||
SERVICE_NAME_USB);
|
||||
public static final List<String> SUGGESTION_FINDERS = List.of(SERVICE_NAME_IP, SERVICE_NAME_MDNS, SERVICE_NAME_SDDP,
|
||||
SERVICE_NAME_UPNP, SERVICE_NAME_USB);
|
||||
|
||||
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_TYPE_USB);
|
||||
SERVICE_NAME_MDNS, SERVICE_TYPE_MDNS, SERVICE_NAME_SDDP, SERVICE_TYPE_SDDP, SERVICE_NAME_UPNP,
|
||||
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,
|
||||
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);
|
||||
}
|
||||
|
34
bundles/org.openhab.core.config.discovery.sddp/.classpath
Normal file
34
bundles/org.openhab.core.config.discovery.sddp/.classpath
Normal 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>
|
23
bundles/org.openhab.core.config.discovery.sddp/.project
Normal file
23
bundles/org.openhab.core.config.discovery.sddp/.project
Normal 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>
|
14
bundles/org.openhab.core.config.discovery.sddp/NOTICE
Normal file
14
bundles/org.openhab.core.config.discovery.sddp/NOTICE
Normal 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
|
||||
|
25
bundles/org.openhab.core.config.discovery.sddp/pom.xml
Normal file
25
bundles/org.openhab.core.config.discovery.sddp/pom.xml
Normal 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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -34,6 +34,12 @@
|
||||
<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>
|
||||
</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">
|
||||
<advanced>true</advanced>
|
||||
<label>USB Suggestion Finder</label>
|
||||
|
@ -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.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.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.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
|
||||
|
@ -34,9 +34,11 @@
|
||||
<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.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.usb</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.linuxsysfs</module>
|
||||
<module>org.openhab.core.config.discovery.usbserial.ser2net</module>
|
||||
|
@ -105,6 +105,18 @@
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon.usb/${project.version}</bundle>
|
||||
</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>kar</feature>
|
||||
<feature>openhab-core-base</feature>
|
||||
|
Loading…
Reference in New Issue
Block a user