From 62a50a409a9c9b90ef0617556b11991b73f94d3d Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Thu, 7 Dec 2023 16:32:33 +0000 Subject: [PATCH] Service to find suggested addons to install (#3806) Signed-off-by: Andrew Fiddian-Green Co-authored-by: Mark Herwege --- bom/openhab-core/pom.xml | 18 ++ .../.classpath | 29 ++ .../.project | 23 ++ .../NOTICE | 14 + .../pom.xml | 34 +++ .../discovery/addon/mdns/MDNSAddonFinder.java | 183 +++++++++++++ .../mdns/tests/MDNSAddonFinderTests.java | 125 +++++++++ .../.classpath | 29 ++ .../.project | 23 ++ .../NOTICE | 14 + .../pom.xml | 29 ++ .../discovery/addon/upnp/UpnpAddonFinder.java | 247 ++++++++++++++++++ .../upnp/tests/UpnpAddonFinderTests.java | 149 +++++++++++ .../.classpath | 29 ++ .../.project | 23 ++ .../NOTICE | 14 + .../pom.xml | 24 ++ .../config/discovery/addon/AddonFinder.java | 48 ++++ .../discovery/addon/AddonFinderConstants.java | 46 ++++ .../discovery/addon/AddonFinderService.java | 43 +++ .../addon/AddonSuggestionService.java | 200 ++++++++++++++ .../discovery/addon/BaseAddonFinder.java | 60 +++++ .../tests/AddonSuggestionServiceTests.java | 196 ++++++++++++++ bundles/org.openhab.core.io.rest.core/pom.xml | 5 + .../core/internal/addons/AddonResource.java | 19 +- bundles/org.openhab.core.karaf/pom.xml | 5 + .../internal/KarafAddonFinderService.java | 63 +++++ .../karaf/internal/KarafAddonService.java | 3 +- .../main/resources/OH-INF/config/addons.xml | 12 + bundles/pom.xml | 3 + .../openhab-core/src/main/feature/feature.xml | 23 ++ 31 files changed, 1730 insertions(+), 3 deletions(-) create mode 100644 bundles/org.openhab.core.config.discovery.addon.mdns/.classpath create mode 100644 bundles/org.openhab.core.config.discovery.addon.mdns/.project create mode 100644 bundles/org.openhab.core.config.discovery.addon.mdns/NOTICE create mode 100644 bundles/org.openhab.core.config.discovery.addon.mdns/pom.xml create mode 100644 bundles/org.openhab.core.config.discovery.addon.mdns/src/main/java/org/openhab/core/config/discovery/addon/mdns/MDNSAddonFinder.java create mode 100644 bundles/org.openhab.core.config.discovery.addon.mdns/src/test/java/org/openhab/core/config/discovery/addon/mdns/tests/MDNSAddonFinderTests.java create mode 100644 bundles/org.openhab.core.config.discovery.addon.upnp/.classpath create mode 100644 bundles/org.openhab.core.config.discovery.addon.upnp/.project create mode 100644 bundles/org.openhab.core.config.discovery.addon.upnp/NOTICE create mode 100644 bundles/org.openhab.core.config.discovery.addon.upnp/pom.xml create mode 100644 bundles/org.openhab.core.config.discovery.addon.upnp/src/main/java/org/openhab/core/config/discovery/addon/upnp/UpnpAddonFinder.java create mode 100644 bundles/org.openhab.core.config.discovery.addon.upnp/src/test/java/org/openhab/core/config/discovery/addon/upnp/tests/UpnpAddonFinderTests.java create mode 100644 bundles/org.openhab.core.config.discovery.addon/.classpath create mode 100644 bundles/org.openhab.core.config.discovery.addon/.project create mode 100644 bundles/org.openhab.core.config.discovery.addon/NOTICE create mode 100644 bundles/org.openhab.core.config.discovery.addon/pom.xml create mode 100644 bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinder.java create mode 100644 bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinderConstants.java create mode 100644 bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinderService.java create mode 100644 bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonSuggestionService.java create mode 100644 bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/BaseAddonFinder.java create mode 100644 bundles/org.openhab.core.config.discovery.addon/src/test/java/org/openhab/core/config/discovery/addon/tests/AddonSuggestionServiceTests.java create mode 100644 bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/KarafAddonFinderService.java diff --git a/bom/openhab-core/pom.xml b/bom/openhab-core/pom.xml index e8f72da93..4a96fcf9a 100644 --- a/bom/openhab-core/pom.xml +++ b/bom/openhab-core/pom.xml @@ -304,6 +304,24 @@ ${project.version} compile + + org.openhab.core.bundles + org.openhab.core.config.discovery.addon + ${project.version} + compile + + + org.openhab.core.bundles + org.openhab.core.config.discovery.addon.mdns + ${project.version} + compile + + + org.openhab.core.bundles + org.openhab.core.config.discovery.addon.upnp + ${project.version} + compile + org.openhab.core.bundles org.openhab.core.config.discovery.mdns diff --git a/bundles/org.openhab.core.config.discovery.addon.mdns/.classpath b/bundles/org.openhab.core.config.discovery.addon.mdns/.classpath new file mode 100644 index 000000000..d3d6b3c11 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.mdns/.classpath @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.config.discovery.addon.mdns/.project b/bundles/org.openhab.core.config.discovery.addon.mdns/.project new file mode 100644 index 000000000..00932f9e0 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.mdns/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.config.discovery.addon.mdns + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.jdt.core.javanature + + diff --git a/bundles/org.openhab.core.config.discovery.addon.mdns/NOTICE b/bundles/org.openhab.core.config.discovery.addon.mdns/NOTICE new file mode 100644 index 000000000..6c17d0d8a --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.mdns/NOTICE @@ -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 + diff --git a/bundles/org.openhab.core.config.discovery.addon.mdns/pom.xml b/bundles/org.openhab.core.config.discovery.addon.mdns/pom.xml new file mode 100644 index 000000000..3afd5faef --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.mdns/pom.xml @@ -0,0 +1,34 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 4.1.0-SNAPSHOT + + + org.openhab.core.config.discovery.addon.mdns + + openHAB Core :: Bundles :: mDNS Suggested Add-on Finder + + + + org.openhab.core.bundles + org.openhab.core.config.discovery.mdns + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.config.discovery.addon + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.addon + ${project.version} + + + diff --git a/bundles/org.openhab.core.config.discovery.addon.mdns/src/main/java/org/openhab/core/config/discovery/addon/mdns/MDNSAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.mdns/src/main/java/org/openhab/core/config/discovery/addon/mdns/MDNSAddonFinder.java new file mode 100644 index 000000000..6af2d6466 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.mdns/src/main/java/org/openhab/core/config/discovery/addon/mdns/MDNSAddonFinder.java @@ -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 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 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 getSuggestedAddons() { + Set result = new HashSet<>(); + for (AddonInfo candidate : addonCandidates) { + for (AddonDiscoveryMethod method : candidate.getDiscoveryMethods().stream() + .filter(method -> SERVICE_TYPE.equals(method.getServiceType())).toList()) { + Map matchProperties = method.getMatchProperties().stream() + .collect(Collectors.toMap(property -> property.getName(), property -> property.getPattern())); + + Set 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); + } + } + } +} diff --git a/bundles/org.openhab.core.config.discovery.addon.mdns/src/test/java/org/openhab/core/config/discovery/addon/mdns/tests/MDNSAddonFinderTests.java b/bundles/org.openhab.core.config.discovery.addon.mdns/src/test/java/org/openhab/core/config/discovery/addon/mdns/tests/MDNSAddonFinderTests.java new file mode 100644 index 000000000..d202dea4d --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.mdns/src/test/java/org/openhab/core/config/discovery/addon/mdns/tests/MDNSAddonFinderTests.java @@ -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 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 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()))); + } +} diff --git a/bundles/org.openhab.core.config.discovery.addon.upnp/.classpath b/bundles/org.openhab.core.config.discovery.addon.upnp/.classpath new file mode 100644 index 000000000..d3d6b3c11 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.upnp/.classpath @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.config.discovery.addon.upnp/.project b/bundles/org.openhab.core.config.discovery.addon.upnp/.project new file mode 100644 index 000000000..f86bba378 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.upnp/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.config.discovery.addon.upnp + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.jdt.core.javanature + + diff --git a/bundles/org.openhab.core.config.discovery.addon.upnp/NOTICE b/bundles/org.openhab.core.config.discovery.addon.upnp/NOTICE new file mode 100644 index 000000000..6c17d0d8a --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.upnp/NOTICE @@ -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 + diff --git a/bundles/org.openhab.core.config.discovery.addon.upnp/pom.xml b/bundles/org.openhab.core.config.discovery.addon.upnp/pom.xml new file mode 100644 index 000000000..47f50aa76 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.upnp/pom.xml @@ -0,0 +1,29 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 4.1.0-SNAPSHOT + + + org.openhab.core.config.discovery.addon.upnp + + openHAB Core :: Bundles :: uPnP Suggested Add-on Finder + + + + org.openhab.core.bundles + org.openhab.core.config.discovery.addon + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.addon + ${project.version} + + + diff --git a/bundles/org.openhab.core.config.discovery.addon.upnp/src/main/java/org/openhab/core/config/discovery/addon/upnp/UpnpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.upnp/src/main/java/org/openhab/core/config/discovery/addon/upnp/UpnpAddonFinder.java new file mode 100644 index 000000000..034ac431c --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.upnp/src/main/java/org/openhab/core/config/discovery/addon/upnp/UpnpAddonFinder.java @@ -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 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 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 getSuggestedAddons() { + Set result = new HashSet<>(); + for (AddonInfo candidate : addonCandidates) { + for (AddonDiscoveryMethod method : candidate.getDiscoveryMethods().stream() + .filter(method -> SERVICE_TYPE.equals(method.getServiceType())).toList()) { + Map matchProperties = method.getMatchProperties().stream() + .collect(Collectors.toMap(property -> property.getName(), property -> property.getPattern())); + + Set 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); + } + } +} diff --git a/bundles/org.openhab.core.config.discovery.addon.upnp/src/test/java/org/openhab/core/config/discovery/addon/upnp/tests/UpnpAddonFinderTests.java b/bundles/org.openhab.core.config.discovery.addon.upnp/src/test/java/org/openhab/core/config/discovery/addon/upnp/tests/UpnpAddonFinderTests.java new file mode 100644 index 000000000..8c5bd09a6 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.upnp/src/test/java/org/openhab/core/config/discovery/addon/upnp/tests/UpnpAddonFinderTests.java @@ -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 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 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 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()))); + } +} diff --git a/bundles/org.openhab.core.config.discovery.addon/.classpath b/bundles/org.openhab.core.config.discovery.addon/.classpath new file mode 100644 index 000000000..e9d63b05a --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon/.classpath @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.config.discovery.addon/.project b/bundles/org.openhab.core.config.discovery.addon/.project new file mode 100644 index 000000000..ea7024024 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.config.discovery.addon + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.core.config.discovery.addon/NOTICE b/bundles/org.openhab.core.config.discovery.addon/NOTICE new file mode 100644 index 000000000..6c17d0d8a --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon/NOTICE @@ -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 + diff --git a/bundles/org.openhab.core.config.discovery.addon/pom.xml b/bundles/org.openhab.core.config.discovery.addon/pom.xml new file mode 100644 index 000000000..9b33fcd43 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon/pom.xml @@ -0,0 +1,24 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 4.1.0-SNAPSHOT + + + org.openhab.core.config.discovery.addon + + openHAB Core :: Bundles :: Add-on Suggestion Service + + + + org.openhab.core.bundles + org.openhab.core.addon + ${project.version} + + + diff --git a/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinder.java b/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinder.java new file mode 100644 index 000000000..26eec778a --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinder.java @@ -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 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 candidates); + + /** + * This method should be called from the framework to allow a finder to stop searching for addons and do cleanup. + */ + public void unsetAddonCandidates(); +} diff --git a/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinderConstants.java b/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinderConstants.java new file mode 100644 index 000000000..596498898 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinderConstants.java @@ -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 SUGGESTION_FINDERS = List.of(SERVICE_NAME_MDNS, SERVICE_NAME_UPNP); + public static final Map SUGGESTION_FINDER_CONFIGS = Map.of(SERVICE_NAME_MDNS, CFG_FINDER_MDNS, + SERVICE_NAME_UPNP, CFG_FINDER_UPNP); + public static final Map SUGGESTION_FINDER_FEATURES = Map.of(SERVICE_NAME_MDNS, FEATURE_MDNS, + SERVICE_NAME_UPNP, FEATURE_UPNP); +} diff --git a/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinderService.java b/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinderService.java new file mode 100644 index 000000000..276e27fdc --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonFinderService.java @@ -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); +} diff --git a/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonSuggestionService.java b/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonSuggestionService.java new file mode 100644 index 000000000..5ee86cf1e --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/AddonSuggestionService.java @@ -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 addonInfoProviders = ConcurrentHashMap.newKeySet(); + private final List addonFinders = Collections.synchronizedList(new ArrayList<>()); + private final ConfigurationAdmin configurationAdmin; + private final LocaleProvider localeProvider; + private @Nullable AddonFinderService addonFinderService; + private @Nullable Map config; + private final ScheduledExecutorService scheduler; + private final Map baseFinderConfig = new ConcurrentHashMap<>(); + private final ScheduledFuture syncConfigurationTask; + + @Activate + public AddonSuggestionService(final @Reference ConfigurationAdmin configurationAdmin, + @Reference LocaleProvider localeProvider, @Nullable Map 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 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 cfg = configurationAdmin.getConfiguration(CONFIG_PID).getProperties(); + if (cfg == null) { + return; + } + final Map cfgMap = new HashMap<>(); + final Enumeration 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 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 getSuggestedAddons(@Nullable Locale locale) { + return addonFinders.stream().filter(this::isFinderEnabled).map(f -> f.getSuggestedAddons()) + .flatMap(Collection::stream).collect(Collectors.toSet()); + } +} diff --git a/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/BaseAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/BaseAddonFinder.java new file mode 100644 index 000000000..cb62e5351 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon/src/main/java/org/openhab/core/config/discovery/addon/BaseAddonFinder.java @@ -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 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 addonCandidates = List.of(); + + @Override + public void setAddonCandidates(List candidates) { + addonCandidates = candidates; + } + + @Override + public void unsetAddonCandidates() { + addonCandidates = List.of(); + } + + public abstract String getServiceName(); +} diff --git a/bundles/org.openhab.core.config.discovery.addon/src/test/java/org/openhab/core/config/discovery/addon/tests/AddonSuggestionServiceTests.java b/bundles/org.openhab.core.config.discovery.addon/src/test/java/org/openhab/core/config/discovery/addon/tests/AddonSuggestionServiceTests.java new file mode 100644 index 000000000..0d72ac4d5 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon/src/test/java/org/openhab/core/config/discovery/addon/tests/AddonSuggestionServiceTests.java @@ -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 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 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 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 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 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 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 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 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()))); + } +} diff --git a/bundles/org.openhab.core.io.rest.core/pom.xml b/bundles/org.openhab.core.io.rest.core/pom.xml index e829c11cd..20f2604bb 100644 --- a/bundles/org.openhab.core.io.rest.core/pom.xml +++ b/bundles/org.openhab.core.io.rest.core/pom.xml @@ -35,6 +35,11 @@ org.openhab.core.config.discovery ${project.version} + + org.openhab.core.bundles + org.openhab.core.config.discovery.addon + ${project.version} + org.openhab.core.bundles org.openhab.core.io.rest diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/addons/AddonResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/addons/AddonResource.java index b2b4f8bee..fff95f130 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/addons/AddonResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/addons/AddonResource.java @@ -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) diff --git a/bundles/org.openhab.core.karaf/pom.xml b/bundles/org.openhab.core.karaf/pom.xml index 97267744d..aa2d40cd0 100644 --- a/bundles/org.openhab.core.karaf/pom.xml +++ b/bundles/org.openhab.core.karaf/pom.xml @@ -30,6 +30,11 @@ org.openhab.core.config.core ${project.version} + + org.openhab.core.bundles + org.openhab.core.config.discovery.addon + ${project.version} + org.apache.karaf.features org.apache.karaf.features.core diff --git a/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/KarafAddonFinderService.java b/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/KarafAddonFinderService.java new file mode 100644 index 000000000..815d6eaed --- /dev/null +++ b/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/KarafAddonFinderService.java @@ -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); + } + } +} diff --git a/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/KarafAddonService.java b/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/KarafAddonService.java index 8e4227155..e3b246557 100644 --- a/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/KarafAddonService.java +++ b/bundles/org.openhab.core.karaf/src/main/java/org/openhab/core/karaf/internal/KarafAddonService.java @@ -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)) diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/config/addons.xml b/bundles/org.openhab.core/src/main/resources/OH-INF/config/addons.xml index d036ea139..2c143c391 100644 --- a/bundles/org.openhab.core/src/main/resources/OH-INF/config/addons.xml +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/config/addons.xml @@ -17,6 +17,18 @@ expected. Enabling this option will include these entries in the list of available add-ons. false + + true + + Use UPnP network scan to suggest add-ons. + true + + + true + + Use mDNS network scan to suggest add-ons. + true + diff --git a/bundles/pom.xml b/bundles/pom.xml index 70000056d..ad7441432 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -30,6 +30,9 @@ org.openhab.core.automation.rest org.openhab.core.config.core org.openhab.core.config.discovery + org.openhab.core.config.discovery.addon + org.openhab.core.config.discovery.addon.mdns + org.openhab.core.config.discovery.addon.upnp org.openhab.core.config.discovery.mdns org.openhab.core.config.discovery.usbserial org.openhab.core.config.discovery.usbserial.linuxsysfs diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 5cdcf866a..2f919eb97 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -66,6 +66,29 @@ mvn:org.openhab.core.bundles/org.openhab.core.io.rest/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.io.rest.core/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.io.rest.sse/${project.version} + openhab-core-config-discovery-addon + + + + openhab-core-base + mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon/${project.version} + + + + openhab-core-base + openhab-core-config-discovery-addon + mvn:org.openhab.core.bundles/org.openhab.core.io.transport.mdns/${project.version} + mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon.mdns/${project.version} + openhab.tp;filter:="(feature=jmdns)" + openhab.tp-jmdns + + + + openhab-core-base + openhab-core-config-discovery-addon + mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon.upnp/${project.version} + openhab.tp;filter:="(feature=jupnp)" + openhab.tp-jupnp