mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-10 13:21:53 +01:00
Service to find suggested addons to install (#3806)
Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch> Co-authored-by: Mark Herwege <mark.herwege@telenet.be>
This commit is contained in:
parent
de9912d06b
commit
62a50a409a
@ -304,6 +304,24 @@
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.config.discovery.addon</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.config.discovery.addon.mdns</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>
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.config.discovery.mdns</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.mdns</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.mdns/NOTICE
Normal file
14
bundles/org.openhab.core.config.discovery.addon.mdns/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.mdns/pom.xml
Normal file
34
bundles/org.openhab.core.config.discovery.addon.mdns/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.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.core.config.discovery.addon.mdns</artifactId>
|
||||
|
||||
<name>openHAB Core :: Bundles :: mDNS Suggested Add-on Finder</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.config.discovery.mdns</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<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>
|
||||
</dependencies>
|
||||
</project>
|
@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.addon.mdns;
|
||||
|
||||
import static org.openhab.core.config.discovery.addon.AddonFinderConstants.SERVICE_NAME_MDNS;
|
||||
import static org.openhab.core.config.discovery.addon.AddonFinderConstants.SERVICE_TYPE_MDNS;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.jmdns.ServiceEvent;
|
||||
import javax.jmdns.ServiceInfo;
|
||||
import javax.jmdns.ServiceListener;
|
||||
|
||||
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.common.ThreadPoolManager;
|
||||
import org.openhab.core.config.discovery.addon.AddonFinder;
|
||||
import org.openhab.core.config.discovery.addon.BaseAddonFinder;
|
||||
import org.openhab.core.io.transport.mdns.MDNSClient;
|
||||
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 MDNSAddonFinder} for finding suggested add-ons via mDNS.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
* @author Mark Herwege - refactor to allow uninstall
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = AddonFinder.class, name = MDNSAddonFinder.SERVICE_NAME)
|
||||
public class MDNSAddonFinder extends BaseAddonFinder implements ServiceListener {
|
||||
|
||||
public static final String SERVICE_TYPE = SERVICE_TYPE_MDNS;
|
||||
public static final String SERVICE_NAME = SERVICE_NAME_MDNS;
|
||||
|
||||
private static final String NAME = "name";
|
||||
private static final String APPLICATION = "application";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MDNSAddonFinder.class);
|
||||
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(SERVICE_NAME);
|
||||
private final Map<String, ServiceInfo> services = new ConcurrentHashMap<>();
|
||||
private MDNSClient mdnsClient;
|
||||
|
||||
@Activate
|
||||
public MDNSAddonFinder(@Reference MDNSClient mdnsClient) {
|
||||
this.mdnsClient = mdnsClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given mDNS service to the set of discovered services.
|
||||
*
|
||||
* @param device the mDNS service to be added.
|
||||
*/
|
||||
public void addService(ServiceInfo service, boolean isResolved) {
|
||||
String qualifiedName = service.getQualifiedName();
|
||||
if (isResolved || !services.containsKey(qualifiedName)) {
|
||||
if (services.put(qualifiedName, service) == null) {
|
||||
logger.trace("Added service: {}", qualifiedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
services.clear();
|
||||
unsetAddonCandidates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAddonCandidates(List<AddonInfo> candidates) {
|
||||
// Remove listeners for all service types that are no longer in candidates
|
||||
addonCandidates.stream().filter(c -> !candidates.contains(c))
|
||||
.forEach(c -> c.getDiscoveryMethods().stream().filter(m -> SERVICE_TYPE.equals(m.getServiceType()))
|
||||
.filter(m -> !m.getMdnsServiceType().isEmpty())
|
||||
.forEach(m -> mdnsClient.removeServiceListener(m.getMdnsServiceType(), this)));
|
||||
|
||||
// Add listeners for all service types in candidates
|
||||
super.setAddonCandidates(candidates);
|
||||
addonCandidates
|
||||
.forEach(c -> c.getDiscoveryMethods().stream().filter(m -> SERVICE_TYPE.equals(m.getServiceType()))
|
||||
.filter(m -> !m.getMdnsServiceType().isEmpty()).forEach(m -> {
|
||||
String serviceType = m.getMdnsServiceType();
|
||||
mdnsClient.addServiceListener(serviceType, this);
|
||||
scheduler.submit(() -> mdnsClient.list(serviceType));
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsetAddonCandidates() {
|
||||
addonCandidates.forEach(c -> c.getDiscoveryMethods().stream()
|
||||
.filter(m -> SERVICE_TYPE.equals(m.getServiceType())).filter(m -> !m.getMdnsServiceType().isEmpty())
|
||||
.forEach(m -> mdnsClient.removeServiceListener(m.getMdnsServiceType(), this)));
|
||||
super.unsetAddonCandidates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<AddonInfo> getSuggestedAddons() {
|
||||
Set<AddonInfo> result = new HashSet<>();
|
||||
for (AddonInfo candidate : addonCandidates) {
|
||||
for (AddonDiscoveryMethod method : candidate.getDiscoveryMethods().stream()
|
||||
.filter(method -> SERVICE_TYPE.equals(method.getServiceType())).toList()) {
|
||||
Map<String, Pattern> matchProperties = method.getMatchProperties().stream()
|
||||
.collect(Collectors.toMap(property -> property.getName(), property -> property.getPattern()));
|
||||
|
||||
Set<String> matchPropertyKeys = matchProperties.keySet().stream()
|
||||
.filter(property -> (!NAME.equals(property) && !APPLICATION.equals(property)))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
logger.trace("Checking candidate: {}", candidate.getUID());
|
||||
for (ServiceInfo service : services.values()) {
|
||||
|
||||
logger.trace("Checking service: {}/{}", service.getQualifiedName(), service.getNiceTextString());
|
||||
if (method.getMdnsServiceType().equals(service.getType())
|
||||
&& propertyMatches(matchProperties, NAME, service.getName())
|
||||
&& propertyMatches(matchProperties, APPLICATION, service.getApplication())
|
||||
&& matchPropertyKeys.stream().allMatch(
|
||||
name -> propertyMatches(matchProperties, name, service.getPropertyString(name)))) {
|
||||
result.add(candidate);
|
||||
logger.debug("Suggested add-on found: {}", candidate.getUID());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return SERVICE_NAME;
|
||||
}
|
||||
|
||||
/*
|
||||
* ************ MDNSClient call-back methods ************
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void serviceAdded(@Nullable ServiceEvent event) {
|
||||
if (event != null) {
|
||||
ServiceInfo service = event.getInfo();
|
||||
if (service != null) {
|
||||
addService(service, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(@Nullable ServiceEvent event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(@Nullable ServiceEvent event) {
|
||||
if (event != null) {
|
||||
ServiceInfo service = event.getInfo();
|
||||
if (service != null) {
|
||||
addService(service, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.addon.mdns.tests;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.jmdns.ServiceInfo;
|
||||
|
||||
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.mockito.Mockito;
|
||||
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.AddonFinder;
|
||||
import org.openhab.core.config.discovery.addon.AddonFinderConstants;
|
||||
import org.openhab.core.config.discovery.addon.AddonSuggestionService;
|
||||
import org.openhab.core.config.discovery.addon.mdns.MDNSAddonFinder;
|
||||
import org.openhab.core.io.transport.mdns.MDNSClient;
|
||||
|
||||
/**
|
||||
* JUnit tests for the {@link AddonSuggestionService}.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
* @author Mark Herwege - Adapted to finders in separate packages
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
public class MDNSAddonFinderTests {
|
||||
|
||||
private @NonNullByDefault({}) MDNSClient mdnsClient;
|
||||
private @NonNullByDefault({}) AddonFinder addonFinder;
|
||||
private List<AddonInfo> addonInfos = new ArrayList<>();
|
||||
|
||||
@BeforeAll
|
||||
public void setup() {
|
||||
setupMockMdnsClient();
|
||||
setupAddonInfos();
|
||||
createAddonFinder();
|
||||
}
|
||||
|
||||
private void createAddonFinder() {
|
||||
MDNSAddonFinder mdnsAddonFinder = new MDNSAddonFinder(mdnsClient);
|
||||
assertNotNull(mdnsAddonFinder);
|
||||
|
||||
for (ServiceInfo service : mdnsClient.list("_hue._tcp.local.")) {
|
||||
mdnsAddonFinder.addService(service, true);
|
||||
}
|
||||
for (ServiceInfo service : mdnsClient.list("_printer._tcp.local.")) {
|
||||
mdnsAddonFinder.addService(service, true);
|
||||
}
|
||||
|
||||
addonFinder = mdnsAddonFinder;
|
||||
}
|
||||
|
||||
private void setupMockMdnsClient() {
|
||||
// create the mock
|
||||
mdnsClient = mock(MDNSClient.class, Mockito.RETURNS_DEEP_STUBS);
|
||||
when(mdnsClient.list(anyString())).thenReturn(new ServiceInfo[] {});
|
||||
ServiceInfo hueService = ServiceInfo.create("hue", "hue", 0, 0, 0, false, "hue service");
|
||||
when(mdnsClient.list(eq("_hue._tcp.local."))).thenReturn(new ServiceInfo[] { hueService });
|
||||
ServiceInfo hpService = ServiceInfo.create("printer", "hpprinter", 0, 0, 0, false, "hp printer service");
|
||||
hpService.setText(Map.of("ty", "hp printer", "rp", "anything"));
|
||||
when(mdnsClient.list(eq("_printer._tcp.local."))).thenReturn(new ServiceInfo[] { hpService });
|
||||
|
||||
// check that it works
|
||||
assertNotNull(mdnsClient);
|
||||
ServiceInfo[] result;
|
||||
result = mdnsClient.list("_printer._tcp.local.");
|
||||
assertEquals(1, result.length);
|
||||
assertEquals("hpprinter", result[0].getName());
|
||||
assertEquals(2, Collections.list(result[0].getPropertyNames()).size());
|
||||
assertEquals("hp printer", result[0].getPropertyString("ty"));
|
||||
result = mdnsClient.list("_hue._tcp.local.");
|
||||
assertEquals(1, result.length);
|
||||
assertEquals("hue", result[0].getName());
|
||||
result = mdnsClient.list("aardvark");
|
||||
assertEquals(0, result.length);
|
||||
}
|
||||
|
||||
private void setupAddonInfos() {
|
||||
AddonDiscoveryMethod hp = new AddonDiscoveryMethod().setServiceType(AddonFinderConstants.SERVICE_TYPE_MDNS)
|
||||
.setMatchProperties(
|
||||
List.of(new AddonMatchProperty("rp", ".*"), new AddonMatchProperty("ty", "hp (.*)")))
|
||||
.setMdnsServiceType("_printer._tcp.local.");
|
||||
addonInfos.add(AddonInfo.builder("hpprinter", "binding").withName("HP").withDescription("HP Printer")
|
||||
.withDiscoveryMethods(List.of(hp)).build());
|
||||
|
||||
AddonDiscoveryMethod hue = new AddonDiscoveryMethod().setServiceType(AddonFinderConstants.SERVICE_TYPE_MDNS)
|
||||
.setMdnsServiceType("_hue._tcp.local.");
|
||||
addonInfos.add(AddonInfo.builder("hue", "binding").withName("Hue").withDescription("Hue Bridge")
|
||||
.withDiscoveryMethods(List.of(hue)).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuggestedAddons() {
|
||||
addonFinder.setAddonCandidates(addonInfos);
|
||||
Set<AddonInfo> addons = addonFinder.getSuggestedAddons();
|
||||
assertEquals(2, addons.size());
|
||||
assertFalse(addons.stream().anyMatch(a -> "aardvark".equals(a.getUID())));
|
||||
assertTrue(addons.stream().anyMatch(a -> "binding-hue".equals(a.getUID())));
|
||||
assertTrue(addons.stream().anyMatch(a -> "binding-hpprinter".equals(a.getUID())));
|
||||
}
|
||||
}
|
@ -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.upnp/NOTICE
Normal file
14
bundles/org.openhab.core.config.discovery.addon.upnp/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
|
||||
|
29
bundles/org.openhab.core.config.discovery.addon.upnp/pom.xml
Normal file
29
bundles/org.openhab.core.config.discovery.addon.upnp/pom.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?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.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.core.config.discovery.addon.upnp</artifactId>
|
||||
|
||||
<name>openHAB Core :: Bundles :: uPnP 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>
|
||||
</dependencies>
|
||||
</project>
|
@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.addon.upnp;
|
||||
|
||||
import static org.openhab.core.config.discovery.addon.AddonFinderConstants.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.jupnp.UpnpService;
|
||||
import org.jupnp.model.meta.DeviceDetails;
|
||||
import org.jupnp.model.meta.LocalDevice;
|
||||
import org.jupnp.model.meta.ManufacturerDetails;
|
||||
import org.jupnp.model.meta.ModelDetails;
|
||||
import org.jupnp.model.meta.RemoteDevice;
|
||||
import org.jupnp.model.meta.RemoteDeviceIdentity;
|
||||
import org.jupnp.model.types.DeviceType;
|
||||
import org.jupnp.model.types.UDN;
|
||||
import org.jupnp.registry.Registry;
|
||||
import org.jupnp.registry.RegistryListener;
|
||||
import org.openhab.core.addon.AddonDiscoveryMethod;
|
||||
import org.openhab.core.addon.AddonInfo;
|
||||
import org.openhab.core.config.discovery.addon.AddonFinder;
|
||||
import org.openhab.core.config.discovery.addon.BaseAddonFinder;
|
||||
import org.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 UpnpAddonFinder} for finding suggested Addons via UPnP.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
* @author Mark Herwege - refactor to allow uninstall
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = AddonFinder.class, name = UpnpAddonFinder.SERVICE_NAME)
|
||||
public class UpnpAddonFinder extends BaseAddonFinder implements RegistryListener {
|
||||
|
||||
public static final String SERVICE_TYPE = SERVICE_TYPE_UPNP;
|
||||
public static final String SERVICE_NAME = SERVICE_NAME_UPNP;
|
||||
|
||||
private static final String DEVICE_TYPE = "deviceType";
|
||||
private static final String MANUFACTURER = "manufacturer";
|
||||
private static final String MANUFACTURER_URI = "manufacturerURI";
|
||||
private static final String MODEL_NAME = "modelName";
|
||||
private static final String MODEL_NUMBER = "modelNumber";
|
||||
private static final String MODEL_DESCRIPTION = "modelDescription";
|
||||
private static final String MODEL_URI = "modelURI";
|
||||
private static final String SERIAL_NUMBER = "serialNumber";
|
||||
private static final String FRIENDLY_NAME = "friendlyName";
|
||||
|
||||
private static final Set<String> SUPPORTED_PROPERTIES = Set.of(DEVICE_TYPE, MANUFACTURER, MANUFACTURER_URI,
|
||||
MODEL_NAME, MODEL_NUMBER, MODEL_DESCRIPTION, MODEL_URI, SERIAL_NUMBER, FRIENDLY_NAME);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(UpnpAddonFinder.class);
|
||||
private final Map<String, RemoteDevice> devices = new ConcurrentHashMap<>();
|
||||
private UpnpService upnpService;
|
||||
|
||||
@Activate
|
||||
public UpnpAddonFinder(@Reference UpnpService upnpService) {
|
||||
this.upnpService = upnpService;
|
||||
|
||||
Registry registry = upnpService.getRegistry();
|
||||
for (RemoteDevice device : registry.getRemoteDevices()) {
|
||||
remoteDeviceAdded(registry, device);
|
||||
}
|
||||
registry.addListener(this);
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
unsetAddonCandidates();
|
||||
|
||||
UpnpService upnpService = this.upnpService;
|
||||
upnpService.getRegistry().removeListener(this);
|
||||
|
||||
devices.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given UPnP remote device to the set of discovered devices.
|
||||
*
|
||||
* @param device the UPnP remote device to be added.
|
||||
*/
|
||||
private void addDevice(RemoteDevice device) {
|
||||
RemoteDeviceIdentity identity = device.getIdentity();
|
||||
if (identity != null) {
|
||||
UDN udn = identity.getUdn();
|
||||
if (udn != null) {
|
||||
String udnString = udn.getIdentifierString();
|
||||
if (devices.put(udnString, device) == null) {
|
||||
logger.trace("Added device: {}", device.getDisplayString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<AddonInfo> getSuggestedAddons() {
|
||||
Set<AddonInfo> result = new HashSet<>();
|
||||
for (AddonInfo candidate : addonCandidates) {
|
||||
for (AddonDiscoveryMethod method : candidate.getDiscoveryMethods().stream()
|
||||
.filter(method -> SERVICE_TYPE.equals(method.getServiceType())).toList()) {
|
||||
Map<String, Pattern> matchProperties = method.getMatchProperties().stream()
|
||||
.collect(Collectors.toMap(property -> property.getName(), property -> property.getPattern()));
|
||||
|
||||
Set<String> propertyNames = new HashSet<>(matchProperties.keySet());
|
||||
propertyNames.removeAll(SUPPORTED_PROPERTIES);
|
||||
|
||||
if (!propertyNames.isEmpty()) {
|
||||
logger.warn("Add-on '{}' addon.xml file contains unsupported 'match-property' [{}]",
|
||||
candidate.getUID(), String.join(",", propertyNames));
|
||||
break;
|
||||
}
|
||||
|
||||
logger.trace("Checking candidate: {}", candidate.getUID());
|
||||
for (RemoteDevice device : devices.values()) {
|
||||
|
||||
String deviceType = null;
|
||||
String serialNumber = null;
|
||||
String friendlyName = null;
|
||||
String manufacturer = null;
|
||||
String manufacturerURI = null;
|
||||
String modelName = null;
|
||||
String modelNumber = null;
|
||||
String modelDescription = null;
|
||||
String modelURI = null;
|
||||
|
||||
DeviceType devType = device.getType();
|
||||
if (devType != null) {
|
||||
deviceType = devType.getType();
|
||||
}
|
||||
|
||||
DeviceDetails devDetails = device.getDetails();
|
||||
if (devDetails != null) {
|
||||
friendlyName = devDetails.getFriendlyName();
|
||||
serialNumber = devDetails.getSerialNumber();
|
||||
|
||||
ManufacturerDetails mfrDetails = devDetails.getManufacturerDetails();
|
||||
if (mfrDetails != null) {
|
||||
URI mfrUri = mfrDetails.getManufacturerURI();
|
||||
manufacturer = mfrDetails.getManufacturer();
|
||||
manufacturerURI = mfrUri != null ? mfrUri.toString() : null;
|
||||
}
|
||||
|
||||
ModelDetails modDetails = devDetails.getModelDetails();
|
||||
if (modDetails != null) {
|
||||
URI modUri = modDetails.getModelURI();
|
||||
modelName = modDetails.getModelName();
|
||||
modelDescription = modDetails.getModelDescription();
|
||||
modelNumber = modDetails.getModelNumber();
|
||||
modelURI = modUri != null ? modUri.toString() : null;
|
||||
}
|
||||
}
|
||||
|
||||
logger.trace("Checking device: {}", device.getDisplayString());
|
||||
if (propertyMatches(matchProperties, DEVICE_TYPE, deviceType)
|
||||
&& propertyMatches(matchProperties, MANUFACTURER, manufacturer)
|
||||
&& propertyMatches(matchProperties, MANUFACTURER_URI, manufacturerURI)
|
||||
&& propertyMatches(matchProperties, MODEL_NAME, modelName)
|
||||
&& propertyMatches(matchProperties, MODEL_NUMBER, modelNumber)
|
||||
&& propertyMatches(matchProperties, MODEL_DESCRIPTION, modelDescription)
|
||||
&& propertyMatches(matchProperties, MODEL_URI, modelURI)
|
||||
&& propertyMatches(matchProperties, SERIAL_NUMBER, serialNumber)
|
||||
&& propertyMatches(matchProperties, FRIENDLY_NAME, friendlyName)) {
|
||||
result.add(candidate);
|
||||
logger.debug("Suggested addon found: {}", candidate.getUID());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return SERVICE_NAME;
|
||||
}
|
||||
|
||||
/*
|
||||
* ************ UpnpService call-back methods ************
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void afterShutdown() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeShutdown(@Nullable Registry registry) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void localDeviceAdded(@Nullable Registry registry, @Nullable LocalDevice localDevice) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void localDeviceRemoved(@Nullable Registry registry, @Nullable LocalDevice localDevice) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteDeviceAdded(@Nullable Registry registry, @Nullable RemoteDevice remoteDevice) {
|
||||
if (remoteDevice != null) {
|
||||
addDevice(remoteDevice);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteDeviceDiscoveryFailed(@Nullable Registry registry, @Nullable RemoteDevice remoteDevice,
|
||||
@Nullable Exception exception) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteDeviceDiscoveryStarted(@Nullable Registry registry, @Nullable RemoteDevice remoteDevice) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteDeviceRemoved(@Nullable Registry registry, @Nullable RemoteDevice remoteDevice) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteDeviceUpdated(@Nullable Registry registry, @Nullable RemoteDevice remoteDevice) {
|
||||
if (remoteDevice != null) {
|
||||
addDevice(remoteDevice);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.addon.upnp.tests;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
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.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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.jupnp.UpnpService;
|
||||
import org.jupnp.model.ValidationException;
|
||||
import org.jupnp.model.meta.DeviceDetails;
|
||||
import org.jupnp.model.meta.ManufacturerDetails;
|
||||
import org.jupnp.model.meta.ModelDetails;
|
||||
import org.jupnp.model.meta.RemoteDevice;
|
||||
import org.jupnp.model.meta.RemoteDeviceIdentity;
|
||||
import org.jupnp.model.meta.RemoteService;
|
||||
import org.jupnp.model.types.DeviceType;
|
||||
import org.jupnp.model.types.UDN;
|
||||
import org.mockito.Mockito;
|
||||
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.AddonFinder;
|
||||
import org.openhab.core.config.discovery.addon.AddonFinderConstants;
|
||||
import org.openhab.core.config.discovery.addon.upnp.UpnpAddonFinder;
|
||||
|
||||
/**
|
||||
* JUnit tests for the {@link UpnpAddonFinder}.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
* @author Mark Herwege - Adapted to finders in separate packages
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
public class UpnpAddonFinderTests {
|
||||
|
||||
private @NonNullByDefault({}) UpnpService upnpService;
|
||||
private @NonNullByDefault({}) AddonFinder addonFinder;
|
||||
private List<AddonInfo> addonInfos = new ArrayList<>();
|
||||
|
||||
@BeforeAll
|
||||
public void setup() {
|
||||
setupMockUpnpService();
|
||||
setupAddonInfos();
|
||||
createAddonFinder();
|
||||
}
|
||||
|
||||
private void createAddonFinder() {
|
||||
UpnpAddonFinder upnpAddonFinder = new UpnpAddonFinder(upnpService);
|
||||
assertNotNull(upnpAddonFinder);
|
||||
|
||||
addonFinder = upnpAddonFinder;
|
||||
}
|
||||
|
||||
private void setupMockUpnpService() {
|
||||
// create the mock
|
||||
upnpService = mock(UpnpService.class, Mockito.RETURNS_DEEP_STUBS);
|
||||
URL url = null;
|
||||
try {
|
||||
url = new URL("http://www.openhab.org/");
|
||||
} catch (MalformedURLException e) {
|
||||
fail("MalformedURLException");
|
||||
}
|
||||
UDN udn = new UDN("udn");
|
||||
InetAddress address = null;
|
||||
try {
|
||||
address = InetAddress.getByName("127.0.0.1");
|
||||
} catch (UnknownHostException e) {
|
||||
fail("UnknownHostException");
|
||||
}
|
||||
RemoteDeviceIdentity identity = new RemoteDeviceIdentity(udn, 0, url, new byte[] {}, address);
|
||||
DeviceType type = new DeviceType("nameSpace", "type");
|
||||
ManufacturerDetails manDetails = new ManufacturerDetails("manufacturer", "manufacturerURI");
|
||||
ModelDetails modDetails = new ModelDetails("Philips hue bridge", "modelDescription", "modelNumber", "modelURI");
|
||||
DeviceDetails devDetails = new DeviceDetails("friendlyName", manDetails, modDetails, "serialNumber",
|
||||
"000123456789");
|
||||
List<@Nullable RemoteDevice> remoteDevices = new ArrayList<>();
|
||||
try {
|
||||
remoteDevices.add(new RemoteDevice(identity, type, devDetails, (RemoteService) null));
|
||||
} catch (ValidationException e1) {
|
||||
fail("ValidationException");
|
||||
}
|
||||
when(upnpService.getRegistry().getRemoteDevices()).thenReturn(remoteDevices);
|
||||
|
||||
// check that it works
|
||||
assertNotNull(upnpService);
|
||||
List<RemoteDevice> result = new ArrayList<>(upnpService.getRegistry().getRemoteDevices());
|
||||
assertEquals(1, result.size());
|
||||
RemoteDevice device = result.get(0);
|
||||
assertEquals("manufacturer", device.getDetails().getManufacturerDetails().getManufacturer());
|
||||
assertEquals("serialNumber", device.getDetails().getSerialNumber());
|
||||
}
|
||||
|
||||
private void setupAddonInfos() {
|
||||
AddonDiscoveryMethod hue = new AddonDiscoveryMethod().setServiceType(AddonFinderConstants.SERVICE_TYPE_UPNP)
|
||||
.setMatchProperties(List.of(new AddonMatchProperty("modelName", "Philips hue bridge")));
|
||||
addonInfos.add(AddonInfo.builder("hue", "binding").withName("Hue").withDescription("Hue Bridge")
|
||||
.withDiscoveryMethods(List.of(hue)).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuggestedAddons() {
|
||||
addonFinder.setAddonCandidates(addonInfos);
|
||||
Set<AddonInfo> addons = addonFinder.getSuggestedAddons();
|
||||
assertEquals(1, addons.size());
|
||||
assertFalse(addons.stream().anyMatch(a -> "aardvark".equals(a.getUID())));
|
||||
assertTrue(addons.stream().anyMatch(a -> "binding-hue".equals(a.getUID())));
|
||||
}
|
||||
}
|
29
bundles/org.openhab.core.config.discovery.addon/.classpath
Normal file
29
bundles/org.openhab.core.config.discovery.addon/.classpath
Normal 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="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="test" value="true"/>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
23
bundles/org.openhab.core.config.discovery.addon/.project
Normal file
23
bundles/org.openhab.core.config.discovery.addon/.project
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.core.config.discovery.addon</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.addon/NOTICE
Normal file
14
bundles/org.openhab.core.config.discovery.addon/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
|
||||
|
24
bundles/org.openhab.core.config.discovery.addon/pom.xml
Normal file
24
bundles/org.openhab.core.config.discovery.addon/pom.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?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.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.core.config.discovery.addon</artifactId>
|
||||
|
||||
<name>openHAB Core :: Bundles :: Add-on Suggestion Service</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.addon</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.addon;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.addon.AddonInfo;
|
||||
|
||||
/**
|
||||
* This is a {@link AddonFinder} interface for classes that find add-ons that are suggested to be installed.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface AddonFinder {
|
||||
|
||||
/**
|
||||
* The framework calls this method to scan through the candidate list of {@link AddonInfo} and return a subset of
|
||||
* those that it suggests to be installed.
|
||||
*/
|
||||
public Set<AddonInfo> getSuggestedAddons();
|
||||
|
||||
/**
|
||||
* The framework calls this method to provide a list of {@link AddonInfo} elements which contain potential
|
||||
* candidates that this finder can iterate over in order to detect which ones to return via the
|
||||
* {@code getSuggestedAddons()} method.
|
||||
*
|
||||
* @param candidates a list of AddonInfo candidates.
|
||||
*/
|
||||
public void setAddonCandidates(List<AddonInfo> candidates);
|
||||
|
||||
/**
|
||||
* This method should be called from the framework to allow a finder to stop searching for addons and do cleanup.
|
||||
*/
|
||||
public void unsetAddonCandidates();
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.addon;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* This {@link AddonFinderConstants} contains constants describing addon finders available in core.
|
||||
*
|
||||
* @author Mark Herwege - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AddonFinderConstants {
|
||||
|
||||
private static final String ADDON_SUGGESTION_FINDER = "-addon-suggestion-finder";
|
||||
private static final String ADDON_SUGGESTION_FINDER_FEATURE = "openhab-core-config-discovery-addon-";
|
||||
|
||||
public static final String SERVICE_TYPE_MDNS = "mdns";
|
||||
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 FEATURE_MDNS = ADDON_SUGGESTION_FINDER_FEATURE + SERVICE_TYPE_MDNS;
|
||||
|
||||
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;
|
||||
public static final String FEATURE_UPNP = ADDON_SUGGESTION_FINDER_FEATURE + SERVICE_TYPE_UPNP;
|
||||
|
||||
public static final List<String> SUGGESTION_FINDERS = List.of(SERVICE_NAME_MDNS, SERVICE_NAME_UPNP);
|
||||
public static final Map<String, String> SUGGESTION_FINDER_CONFIGS = Map.of(SERVICE_NAME_MDNS, CFG_FINDER_MDNS,
|
||||
SERVICE_NAME_UPNP, CFG_FINDER_UPNP);
|
||||
public static final Map<String, String> SUGGESTION_FINDER_FEATURES = Map.of(SERVICE_NAME_MDNS, FEATURE_MDNS,
|
||||
SERVICE_NAME_UPNP, FEATURE_UPNP);
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.addon;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Classes implementing this interface can be registered as an OSGi service in order to provide functionality for
|
||||
* managing add-on suggestion finders, such as installing and uninstalling them.
|
||||
*
|
||||
* @author Mark Herwege - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface AddonFinderService {
|
||||
|
||||
/**
|
||||
* Installs the given add-on suggestion finder.
|
||||
*
|
||||
* This can be a long running process. The framework makes sure that this is called within a separate thread.
|
||||
*
|
||||
* @param id the id of the add-on suggestion finder to install
|
||||
*/
|
||||
void install(String id);
|
||||
|
||||
/**
|
||||
* Uninstalls the given add-on suggestion finder.
|
||||
*
|
||||
* This can be a long running process. The framework makes sure that this is called within a separate thread.
|
||||
*
|
||||
* @param id the id of the add-on suggestion finder to uninstall
|
||||
*/
|
||||
void uninstall(String id);
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.addon;
|
||||
|
||||
import static org.openhab.core.config.discovery.addon.AddonFinderConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Dictionary;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
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.addon.AddonInfo;
|
||||
import org.openhab.core.addon.AddonInfoProvider;
|
||||
import org.openhab.core.common.ThreadPoolManager;
|
||||
import org.openhab.core.config.core.ConfigParser;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.osgi.service.cm.ConfigurationAdmin;
|
||||
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.osgi.service.component.annotations.ReferencePolicyOption;
|
||||
|
||||
/**
|
||||
* This is a {@link AddonSuggestionService} which discovers suggested add-ons for the user to install.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
* @author Mark Herwege - Install/remove finders
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true, service = AddonSuggestionService.class, name = AddonSuggestionService.SERVICE_NAME, configurationPid = AddonSuggestionService.CONFIG_PID)
|
||||
public class AddonSuggestionService implements AutoCloseable {
|
||||
|
||||
public static final String SERVICE_NAME = "addon-suggestion-service";
|
||||
public static final String CONFIG_PID = "org.openhab.addons";
|
||||
|
||||
private final Set<AddonInfoProvider> addonInfoProviders = ConcurrentHashMap.newKeySet();
|
||||
private final List<AddonFinder> addonFinders = Collections.synchronizedList(new ArrayList<>());
|
||||
private final ConfigurationAdmin configurationAdmin;
|
||||
private final LocaleProvider localeProvider;
|
||||
private @Nullable AddonFinderService addonFinderService;
|
||||
private @Nullable Map<String, Object> config;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final Map<String, Boolean> baseFinderConfig = new ConcurrentHashMap<>();
|
||||
private final ScheduledFuture<?> syncConfigurationTask;
|
||||
|
||||
@Activate
|
||||
public AddonSuggestionService(final @Reference ConfigurationAdmin configurationAdmin,
|
||||
@Reference LocaleProvider localeProvider, @Nullable Map<String, Object> config) {
|
||||
this.configurationAdmin = configurationAdmin;
|
||||
this.localeProvider = localeProvider;
|
||||
|
||||
SUGGESTION_FINDERS.forEach(f -> baseFinderConfig.put(f, true));
|
||||
modified(config);
|
||||
changed();
|
||||
|
||||
// Changes to the configuration are expected to call the {@link modified} method. This works well when running
|
||||
// in Eclipse. Running in Karaf, the method was not consistently called. Therefore regularly check for changes
|
||||
// in configuration.
|
||||
// This pattern and code was re-used from {@link org.openhab.core.karaf.internal.FeatureInstaller}
|
||||
scheduler = ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
|
||||
syncConfigurationTask = scheduler.scheduleWithFixedDelay(this::syncConfiguration, 1, 1, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
protected void deactivate() {
|
||||
syncConfigurationTask.cancel(true);
|
||||
}
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY)
|
||||
protected void addAddonFinderService(AddonFinderService addonFinderService) {
|
||||
this.addonFinderService = addonFinderService;
|
||||
modified(config);
|
||||
}
|
||||
|
||||
protected void removeAddonFinderService(AddonFinderService addonFinderService) {
|
||||
AddonFinderService finderService = this.addonFinderService;
|
||||
if ((finderService != null) && addonFinderService.getClass().isAssignableFrom(finderService.getClass())) {
|
||||
this.addonFinderService = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Modified
|
||||
public void modified(@Nullable final Map<String, Object> config) {
|
||||
baseFinderConfig.forEach((finder, cfg) -> {
|
||||
String cfgParam = SUGGESTION_FINDER_CONFIGS.get(finder);
|
||||
if (cfgParam != null) {
|
||||
boolean enabled = (config != null)
|
||||
? ConfigParser.valueAsOrElse(config.get(cfgParam), Boolean.class, cfg)
|
||||
: cfg;
|
||||
baseFinderConfig.put(finder, enabled);
|
||||
String feature = SUGGESTION_FINDER_FEATURES.get(finder);
|
||||
AddonFinderService finderService = addonFinderService;
|
||||
if (feature != null && finderService != null) {
|
||||
if (enabled) {
|
||||
scheduler.execute(() -> finderService.install(feature));
|
||||
} else {
|
||||
scheduler.execute(() -> finderService.uninstall(feature));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private void syncConfiguration() {
|
||||
try {
|
||||
Dictionary<String, Object> cfg = configurationAdmin.getConfiguration(CONFIG_PID).getProperties();
|
||||
if (cfg == null) {
|
||||
return;
|
||||
}
|
||||
final Map<String, Object> cfgMap = new HashMap<>();
|
||||
final Enumeration<String> enumeration = cfg.keys();
|
||||
while (enumeration.hasMoreElements()) {
|
||||
final String key = enumeration.nextElement();
|
||||
cfgMap.put(key, cfg.get(key));
|
||||
}
|
||||
if (!cfgMap.equals(config)) {
|
||||
modified(cfgMap);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFinderEnabled(AddonFinder finder) {
|
||||
if (finder instanceof BaseAddonFinder baseFinder) {
|
||||
return baseFinderConfig.getOrDefault(baseFinder.getServiceName(), true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
|
||||
public void addAddonInfoProvider(AddonInfoProvider addonInfoProvider) {
|
||||
addonInfoProviders.add(addonInfoProvider);
|
||||
changed();
|
||||
}
|
||||
|
||||
public void removeAddonInfoProvider(AddonInfoProvider addonInfoProvider) {
|
||||
if (addonInfoProviders.remove(addonInfoProvider)) {
|
||||
changed();
|
||||
}
|
||||
}
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
|
||||
public void addAddonFinder(AddonFinder addonFinder) {
|
||||
addonFinders.add(addonFinder);
|
||||
changed();
|
||||
}
|
||||
|
||||
public void removeAddonFinder(AddonFinder addonFinder) {
|
||||
if (addonFinders.remove(addonFinder)) {
|
||||
changed();
|
||||
}
|
||||
}
|
||||
|
||||
private void changed() {
|
||||
List<AddonInfo> candidates = addonInfoProviders.stream().map(p -> p.getAddonInfos(localeProvider.getLocale()))
|
||||
.flatMap(Collection::stream).toList();
|
||||
addonFinders.stream().filter(this::isFinderEnabled).forEach(f -> f.setAddonCandidates(candidates));
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
addonFinders.clear();
|
||||
addonInfoProviders.clear();
|
||||
}
|
||||
|
||||
public Set<AddonInfo> getSuggestedAddons(@Nullable Locale locale) {
|
||||
return addonFinders.stream().filter(this::isFinderEnabled).map(f -> f.getSuggestedAddons())
|
||||
.flatMap(Collection::stream).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.addon;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.addon.AddonInfo;
|
||||
|
||||
/**
|
||||
* This is a {@link BaseAddonFinder} abstract class for finding suggested add-ons.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class BaseAddonFinder implements AddonFinder {
|
||||
|
||||
/**
|
||||
* Helper method to check if the given {@code propertyName} is in the {@code propertyPatternMap} and if so, the
|
||||
* given {@code propertyValue} matches the respective regular expression {@code Pattern}.
|
||||
*
|
||||
* @param propertyPatternMap map of property names and regex patterns for value matching
|
||||
* @param propertyName
|
||||
* @param propertyValue
|
||||
* @return true a) if the property name exists and the property value is not null and matches the regular
|
||||
* expression, or b) the property name does not exist.
|
||||
*/
|
||||
protected static boolean propertyMatches(Map<String, Pattern> propertyPatternMap, String propertyName,
|
||||
@Nullable String propertyValue) {
|
||||
Pattern pattern = propertyPatternMap.get(propertyName);
|
||||
return pattern == null ? true : propertyValue == null ? false : pattern.matcher(propertyValue).matches();
|
||||
}
|
||||
|
||||
protected volatile List<AddonInfo> addonCandidates = List.of();
|
||||
|
||||
@Override
|
||||
public void setAddonCandidates(List<AddonInfo> candidates) {
|
||||
addonCandidates = candidates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsetAddonCandidates() {
|
||||
addonCandidates = List.of();
|
||||
}
|
||||
|
||||
public abstract String getServiceName();
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.config.discovery.addon.tests;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.openhab.core.config.discovery.addon.AddonFinderConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
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.addon.AddonDiscoveryMethod;
|
||||
import org.openhab.core.addon.AddonInfo;
|
||||
import org.openhab.core.addon.AddonInfoProvider;
|
||||
import org.openhab.core.addon.AddonMatchProperty;
|
||||
import org.openhab.core.config.discovery.addon.AddonFinder;
|
||||
import org.openhab.core.config.discovery.addon.AddonFinderConstants;
|
||||
import org.openhab.core.config.discovery.addon.AddonSuggestionService;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.osgi.service.cm.Configuration;
|
||||
import org.osgi.service.cm.ConfigurationAdmin;
|
||||
|
||||
/**
|
||||
* JUnit tests for the {@link AddonSuggestionService}.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
* @author Mark Herwege - Adapted to finders in separate packages
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
public class AddonSuggestionServiceTests {
|
||||
|
||||
private @NonNullByDefault({}) ConfigurationAdmin configurationAdmin;
|
||||
private @NonNullByDefault({}) LocaleProvider localeProvider;
|
||||
private @NonNullByDefault({}) AddonInfoProvider addonInfoProvider;
|
||||
private @NonNullByDefault({}) AddonFinder mdnsAddonFinder;
|
||||
private @NonNullByDefault({}) AddonFinder upnpAddonFinder;
|
||||
private @NonNullByDefault({}) AddonSuggestionService addonSuggestionService;
|
||||
|
||||
private final Map<String, Object> config = Map.of(AddonFinderConstants.CFG_FINDER_MDNS, true,
|
||||
AddonFinderConstants.CFG_FINDER_UPNP, true);
|
||||
|
||||
@AfterAll
|
||||
public void cleanUp() {
|
||||
assertNotNull(addonSuggestionService);
|
||||
try {
|
||||
addonSuggestionService.close();
|
||||
} catch (Exception e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public void setup() {
|
||||
setupMockConfigurationAdmin();
|
||||
setupMockLocaleProvider();
|
||||
setupMockAddonInfoProvider();
|
||||
setupMockMdnsAddonFinder();
|
||||
setupMockUpnpAddonFinder();
|
||||
addonSuggestionService = createAddonSuggestionService();
|
||||
}
|
||||
|
||||
private AddonSuggestionService createAddonSuggestionService() {
|
||||
AddonSuggestionService addonSuggestionService = new AddonSuggestionService(configurationAdmin, localeProvider,
|
||||
config);
|
||||
assertNotNull(addonSuggestionService);
|
||||
|
||||
addonSuggestionService.addAddonFinder(mdnsAddonFinder);
|
||||
addonSuggestionService.addAddonFinder(upnpAddonFinder);
|
||||
|
||||
return addonSuggestionService;
|
||||
}
|
||||
|
||||
private void setupMockConfigurationAdmin() {
|
||||
// create the mock
|
||||
configurationAdmin = mock(ConfigurationAdmin.class);
|
||||
Configuration configuration = mock(Configuration.class);
|
||||
try {
|
||||
when(configurationAdmin.getConfiguration(any())).thenReturn(configuration);
|
||||
} catch (IOException e) {
|
||||
}
|
||||
when(configuration.getProperties()).thenReturn(null);
|
||||
|
||||
// check that it works
|
||||
assertNotNull(configurationAdmin);
|
||||
try {
|
||||
assertNull(configurationAdmin.getConfiguration(AddonSuggestionService.CONFIG_PID).getProperties());
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
private void setupMockLocaleProvider() {
|
||||
// create the mock
|
||||
localeProvider = mock(LocaleProvider.class);
|
||||
when(localeProvider.getLocale()).thenReturn(Locale.US);
|
||||
|
||||
// check that it works
|
||||
assertNotNull(localeProvider);
|
||||
assertEquals(Locale.US, localeProvider.getLocale());
|
||||
}
|
||||
|
||||
private void setupMockAddonInfoProvider() {
|
||||
AddonDiscoveryMethod hp = new AddonDiscoveryMethod().setServiceType(AddonFinderConstants.SERVICE_TYPE_MDNS)
|
||||
.setMatchProperties(
|
||||
List.of(new AddonMatchProperty("rp", ".*"), new AddonMatchProperty("ty", "hp (.*)")))
|
||||
.setMdnsServiceType("_printer._tcp.local.");
|
||||
|
||||
AddonDiscoveryMethod hue1 = new AddonDiscoveryMethod().setServiceType(AddonFinderConstants.SERVICE_TYPE_UPNP)
|
||||
.setMatchProperties(List.of(new AddonMatchProperty("modelName", "Philips hue bridge")));
|
||||
|
||||
AddonDiscoveryMethod hue2 = new AddonDiscoveryMethod().setServiceType(AddonFinderConstants.SERVICE_TYPE_MDNS)
|
||||
.setMdnsServiceType("_hue._tcp.local.");
|
||||
|
||||
// create the mock
|
||||
addonInfoProvider = mock(AddonInfoProvider.class);
|
||||
Set<AddonInfo> addonInfos = new HashSet<>();
|
||||
addonInfos.add(AddonInfo.builder("hue", "binding").withName("Hue").withDescription("Hue Bridge")
|
||||
.withDiscoveryMethods(List.of(hue1, hue2)).build());
|
||||
|
||||
addonInfos.add(AddonInfo.builder("hpprinter", "binding").withName("HP").withDescription("HP Printer")
|
||||
.withDiscoveryMethods(List.of(hp)).build());
|
||||
when(addonInfoProvider.getAddonInfos(any(Locale.class))).thenReturn(addonInfos);
|
||||
|
||||
// check that it works
|
||||
assertNotNull(addonInfoProvider);
|
||||
Set<AddonInfo> addonInfos2 = addonInfoProvider.getAddonInfos(Locale.US);
|
||||
assertEquals(2, addonInfos2.size());
|
||||
assertTrue(addonInfos2.stream().anyMatch(a -> "binding-hue".equals(a.getUID())));
|
||||
assertTrue(addonInfos2.stream().anyMatch(a -> "binding-hpprinter".equals(a.getUID())));
|
||||
}
|
||||
|
||||
private void setupMockMdnsAddonFinder() {
|
||||
// create the mock
|
||||
mdnsAddonFinder = mock(AddonFinder.class);
|
||||
|
||||
Set<AddonInfo> addonInfos = addonInfoProvider.getAddonInfos(Locale.US).stream().filter(
|
||||
c -> c.getDiscoveryMethods().stream().anyMatch(m -> SERVICE_TYPE_MDNS.equals(m.getServiceType())))
|
||||
.collect(Collectors.toSet());
|
||||
when(mdnsAddonFinder.getSuggestedAddons()).thenReturn(addonInfos);
|
||||
|
||||
// check that it works
|
||||
assertNotNull(mdnsAddonFinder);
|
||||
Set<AddonInfo> addonInfos2 = mdnsAddonFinder.getSuggestedAddons();
|
||||
assertEquals(2, addonInfos2.size());
|
||||
assertTrue(addonInfos2.stream().anyMatch(a -> "binding-hue".equals(a.getUID())));
|
||||
assertTrue(addonInfos2.stream().anyMatch(a -> "binding-hpprinter".equals(a.getUID())));
|
||||
}
|
||||
|
||||
private void setupMockUpnpAddonFinder() {
|
||||
// create the mock
|
||||
upnpAddonFinder = mock(AddonFinder.class);
|
||||
|
||||
Set<AddonInfo> addonInfos = addonInfoProvider.getAddonInfos(Locale.US).stream().filter(
|
||||
c -> c.getDiscoveryMethods().stream().anyMatch(m -> SERVICE_TYPE_UPNP.equals(m.getServiceType())))
|
||||
.collect(Collectors.toSet());
|
||||
when(upnpAddonFinder.getSuggestedAddons()).thenReturn(addonInfos);
|
||||
|
||||
// check that it works
|
||||
assertNotNull(upnpAddonFinder);
|
||||
Set<AddonInfo> addonInfos2 = upnpAddonFinder.getSuggestedAddons();
|
||||
assertEquals(1, addonInfos2.size());
|
||||
assertTrue(addonInfos2.stream().anyMatch(a -> "binding-hue".equals(a.getUID())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuggestedAddons() {
|
||||
addonSuggestionService.addAddonInfoProvider(addonInfoProvider);
|
||||
Set<AddonInfo> addons = addonSuggestionService.getSuggestedAddons(localeProvider.getLocale());
|
||||
assertEquals(2, addons.size());
|
||||
assertFalse(addons.stream().anyMatch(a -> "aardvark".equals(a.getUID())));
|
||||
assertTrue(addons.stream().anyMatch(a -> "binding-hue".equals(a.getUID())));
|
||||
assertTrue(addons.stream().anyMatch(a -> "binding-hpprinter".equals(a.getUID())));
|
||||
}
|
||||
}
|
@ -35,6 +35,11 @@
|
||||
<artifactId>org.openhab.core.config.discovery</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<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.io.rest</artifactId>
|
||||
|
@ -56,6 +56,7 @@ import org.openhab.core.config.core.ConfigDescription;
|
||||
import org.openhab.core.config.core.ConfigDescriptionRegistry;
|
||||
import org.openhab.core.config.core.ConfigUtil;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.config.discovery.addon.AddonSuggestionService;
|
||||
import org.openhab.core.events.Event;
|
||||
import org.openhab.core.events.EventPublisher;
|
||||
import org.openhab.core.io.rest.JSONResponse;
|
||||
@ -120,6 +121,7 @@ public class AddonResource implements RESTResource {
|
||||
private final ConfigurationService configurationService;
|
||||
private final AddonInfoRegistry addonInfoRegistry;
|
||||
private final ConfigDescriptionRegistry configDescriptionRegistry;
|
||||
private final AddonSuggestionService addonSuggestionService;
|
||||
|
||||
private @Context @NonNullByDefault({}) UriInfo uriInfo;
|
||||
|
||||
@ -127,12 +129,14 @@ public class AddonResource implements RESTResource {
|
||||
public AddonResource(final @Reference EventPublisher eventPublisher, final @Reference LocaleService localeService,
|
||||
final @Reference ConfigurationService configurationService,
|
||||
final @Reference AddonInfoRegistry addonInfoRegistry,
|
||||
final @Reference ConfigDescriptionRegistry configDescriptionRegistry) {
|
||||
final @Reference ConfigDescriptionRegistry configDescriptionRegistry,
|
||||
final @Reference AddonSuggestionService addonSuggestionService) {
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.localeService = localeService;
|
||||
this.configurationService = configurationService;
|
||||
this.addonInfoRegistry = addonInfoRegistry;
|
||||
this.configDescriptionRegistry = configDescriptionRegistry;
|
||||
this.addonSuggestionService = addonSuggestionService;
|
||||
}
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
|
||||
@ -178,6 +182,19 @@ public class AddonResource implements RESTResource {
|
||||
return Response.ok(new Stream2JSONInputStream(addonTypeStream)).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/suggestions")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(operationId = "getSuggestedAddons", summary = "Get suggested add-ons to be installed.", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = Addon.class)))), })
|
||||
public Response getSuggestions(
|
||||
@HeaderParam("Accept-Language") @Parameter(description = "language") @Nullable String language) {
|
||||
logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath());
|
||||
Locale locale = localeService.getLocale(language);
|
||||
return Response.ok(new Stream2JSONInputStream(addonSuggestionService.getSuggestedAddons(locale).stream()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/types")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
@ -30,6 +30,11 @@
|
||||
<artifactId>org.openhab.core.config.core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.config.discovery.addon</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.karaf.features</groupId>
|
||||
<artifactId>org.apache.karaf.features.core</artifactId>
|
||||
|
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.karaf.internal;
|
||||
|
||||
import org.apache.karaf.features.FeaturesService;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.config.discovery.addon.AddonFinderService;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This service is an implementation of an openHAB {@link AddonSuggestionFinderService} using the Karaf features
|
||||
* service. This service allows dynamic installation/removal of add-on suggestion finders.
|
||||
*
|
||||
* @author Mark Herwege - Initial contribution
|
||||
*/
|
||||
@Component(name = "org.openhab.core.karafaddonfinders", immediate = true)
|
||||
@NonNullByDefault
|
||||
public class KarafAddonFinderService implements AddonFinderService {
|
||||
private final Logger logger = LoggerFactory.getLogger(KarafAddonFinderService.class);
|
||||
|
||||
private final FeaturesService featuresService;
|
||||
|
||||
@Activate
|
||||
public KarafAddonFinderService(final @Reference FeaturesService featuresService) {
|
||||
this.featuresService = featuresService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void install(String id) {
|
||||
try {
|
||||
if (!featuresService.isInstalled(featuresService.getFeature(id))) {
|
||||
featuresService.installFeature(id);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to install add-on suggestion finder {}", id, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninstall(String id) {
|
||||
try {
|
||||
if (featuresService.isInstalled(featuresService.getFeature(id))) {
|
||||
featuresService.uninstallFeature(id);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to uninstall add-on suggestion finder {}", id, e);
|
||||
}
|
||||
}
|
||||
}
|
@ -133,8 +133,7 @@ public class KarafAddonService implements AddonService {
|
||||
|
||||
AddonInfo addonInfo = addonInfoRegistry.getAddonInfo(uid, locale);
|
||||
|
||||
if (isInstalled && addonInfo != null) {
|
||||
// only enrich if this add-on is installed, otherwise wrong data might be added
|
||||
if (addonInfo != null) {
|
||||
addon = addon.withLabel(addonInfo.getName()).withDescription(addonInfo.getDescription())
|
||||
.withConnection(addonInfo.getConnection()).withCountries(addonInfo.getCountries())
|
||||
.withLink(getDefaultDocumentationLink(type, name))
|
||||
|
@ -17,6 +17,18 @@
|
||||
expected. Enabling this option will include these entries in the list of available add-ons.</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="suggestionFinderUpnp" type="boolean">
|
||||
<advanced>true</advanced>
|
||||
<label>UPnP Suggestion Finder</label>
|
||||
<description>Use UPnP network scan to suggest add-ons.</description>
|
||||
<default>true</default>
|
||||
</parameter>
|
||||
<parameter name="suggestionFinderMdns" type="boolean">
|
||||
<advanced>true</advanced>
|
||||
<label>mDNS Suggestion Finder</label>
|
||||
<description>Use mDNS network scan to suggest add-ons.</description>
|
||||
<default>true</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
||||
|
@ -30,6 +30,9 @@
|
||||
<module>org.openhab.core.automation.rest</module>
|
||||
<module>org.openhab.core.config.core</module>
|
||||
<module>org.openhab.core.config.discovery</module>
|
||||
<module>org.openhab.core.config.discovery.addon</module>
|
||||
<module>org.openhab.core.config.discovery.addon.mdns</module>
|
||||
<module>org.openhab.core.config.discovery.addon.upnp</module>
|
||||
<module>org.openhab.core.config.discovery.mdns</module>
|
||||
<module>org.openhab.core.config.discovery.usbserial</module>
|
||||
<module>org.openhab.core.config.discovery.usbserial.linuxsysfs</module>
|
||||
|
@ -66,6 +66,29 @@
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest/${project.version}</bundle>
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.core/${project.version}</bundle>
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.sse/${project.version}</bundle>
|
||||
<feature>openhab-core-config-discovery-addon</feature>
|
||||
</feature>
|
||||
|
||||
<feature name="openhab-core-config-discovery-addon" version="${project.version}">
|
||||
<feature>openhab-core-base</feature>
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon/${project.version}</bundle>
|
||||
</feature>
|
||||
|
||||
<feature name="openhab-core-config-discovery-addon-mdns" version="${project.version}">
|
||||
<feature>openhab-core-base</feature>
|
||||
<feature>openhab-core-config-discovery-addon</feature>
|
||||
<bundle dependency="true">mvn:org.openhab.core.bundles/org.openhab.core.io.transport.mdns/${project.version}</bundle>
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon.mdns/${project.version}</bundle>
|
||||
<requirement>openhab.tp;filter:="(feature=jmdns)"</requirement>
|
||||
<feature dependency="true">openhab.tp-jmdns</feature>
|
||||
</feature>
|
||||
|
||||
<feature name="openhab-core-config-discovery-addon-upnp" version="${project.version}">
|
||||
<feature>openhab-core-base</feature>
|
||||
<feature>openhab-core-config-discovery-addon</feature>
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon.upnp/${project.version}</bundle>
|
||||
<requirement>openhab.tp;filter:="(feature=jupnp)"</requirement>
|
||||
<feature dependency="true">openhab.tp-jupnp</feature>
|
||||
</feature>
|
||||
|
||||
<feature name="openhab-core-addon-marketplace" version="${project.version}">
|
||||
|
Loading…
Reference in New Issue
Block a user