diff --git a/bom/openhab-core/pom.xml b/bom/openhab-core/pom.xml index 905bfb7e0..a32a982de 100644 --- a/bom/openhab-core/pom.xml +++ b/bom/openhab-core/pom.xml @@ -328,6 +328,12 @@ ${project.version} compile + + org.openhab.core.bundles + org.openhab.core.config.discovery.addon.sddp + ${project.version} + compile + org.openhab.core.bundles org.openhab.core.config.discovery.addon.upnp @@ -346,6 +352,12 @@ ${project.version} compile + + org.openhab.core.bundles + org.openhab.core.config.discovery.sddp + ${project.version} + compile + org.openhab.core.bundles org.openhab.core.config.discovery.usbserial diff --git a/bundles/org.openhab.core.config.discovery.addon.sddp/.classpath b/bundles/org.openhab.core.config.discovery.addon.sddp/.classpath new file mode 100644 index 000000000..d3d6b3c11 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.sddp/.classpath @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.config.discovery.addon.sddp/.project b/bundles/org.openhab.core.config.discovery.addon.sddp/.project new file mode 100644 index 000000000..f86bba378 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.sddp/.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.sddp/NOTICE b/bundles/org.openhab.core.config.discovery.addon.sddp/NOTICE new file mode 100644 index 000000000..6c17d0d8a --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.sddp/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.sddp/pom.xml b/bundles/org.openhab.core.config.discovery.addon.sddp/pom.xml new file mode 100644 index 000000000..f56d06abf --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.sddp/pom.xml @@ -0,0 +1,34 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 4.2.0-SNAPSHOT + + + org.openhab.core.config.discovery.addon.sddp + + openHAB Core :: Bundles :: SDDP 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} + + + org.openhab.core.bundles + org.openhab.core.config.discovery.sddp + ${project.version} + + + diff --git a/bundles/org.openhab.core.config.discovery.addon.sddp/src/main/java/org/openhab/core/config/discovery/addon/sddp/SddpAddonFinder.java b/bundles/org.openhab.core.config.discovery.addon.sddp/src/main/java/org/openhab/core/config/discovery/addon/sddp/SddpAddonFinder.java new file mode 100644 index 000000000..9c8448641 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.sddp/src/main/java/org/openhab/core/config/discovery/addon/sddp/SddpAddonFinder.java @@ -0,0 +1,166 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.config.discovery.addon.sddp; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.addon.AddonDiscoveryMethod; +import org.openhab.core.addon.AddonInfo; +import org.openhab.core.addon.AddonMatchProperty; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.config.discovery.addon.AddonFinder; +import org.openhab.core.config.discovery.addon.AddonFinderConstants; +import org.openhab.core.config.discovery.addon.BaseAddonFinder; +import org.openhab.core.config.discovery.sddp.SddpDevice; +import org.openhab.core.config.discovery.sddp.SddpDeviceParticipant; +import org.openhab.core.config.discovery.sddp.SddpDiscoveryService; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a {@link SddpAddonFinder} for finding suggested Addons via SDDP. + *

+ * It checks the binding's addon.xml 'match-property' elements for the following SDDP properties: + *

  • driver
  • + *
  • host
  • + *
  • ipAddress
  • + *
  • macAddress
  • + *
  • manufacturer
  • + *
  • model
  • + *
  • port
  • + *
  • primaryProxy
  • + *
  • proxies
  • + *
  • type
  • + *

    + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +@Component(service = AddonFinder.class, name = SddpAddonFinder.SERVICE_NAME) +public class SddpAddonFinder extends BaseAddonFinder implements SddpDeviceParticipant { + + public static final String SERVICE_TYPE = AddonFinderConstants.SERVICE_TYPE_SDDP; + public static final String SERVICE_NAME = AddonFinderConstants.SERVICE_NAME_SDDP; + + private static final String DRIVER = "driver"; + private static final String HOST = "host"; + private static final String IP_ADDRESS = "ipAddress"; + private static final String MAC_ADDRESS = "macAddress"; + private static final String MANUFACTURER = "manufacturer"; + private static final String MODEL = "model"; + private static final String PORT = "port"; + private static final String PRIMARY_PROXY = "primaryProxy"; + private static final String PROXIES = "proxies"; + private static final String TYPE = "type"; + + private static final Set SUPPORTED_PROPERTIES = Set.of(DRIVER, HOST, IP_ADDRESS, MAC_ADDRESS, MANUFACTURER, + MODEL, PORT, PRIMARY_PROXY, PROXIES, TYPE); + + private final Logger logger = LoggerFactory.getLogger(SddpAddonFinder.class); + private final Set foundDevices = new HashSet<>(); + + private @Nullable SddpDiscoveryService sddpDiscoveryService = null; + + @Activate + public SddpAddonFinder( + @Reference(service = DiscoveryService.class, target = "(protocol=sddp)") DiscoveryService discoveryService) { + if (discoveryService instanceof SddpDiscoveryService sddpDiscoveryService) { + sddpDiscoveryService.addSddpDeviceParticipant(this); + this.sddpDiscoveryService = sddpDiscoveryService; + } else { + logger.warn("SddpAddonFinder() DiscoveryService is not an SddpDiscoveryService"); + } + } + + @Deactivate + public void deactivate() { + SddpDiscoveryService sddpDiscoveryService = this.sddpDiscoveryService; + if (sddpDiscoveryService != null) { + sddpDiscoveryService.removeSddpDeviceParticipant(this); + this.sddpDiscoveryService = null; + } + unsetAddonCandidates(); + foundDevices.clear(); + } + + @Override + public void deviceAdded(SddpDevice device) { + foundDevices.add(device); + } + + @Override + public void deviceRemoved(SddpDevice device) { + foundDevices.remove(device); + } + + @Override + public String getServiceName() { + return SERVICE_NAME; + } + + @Override + public Set 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(AddonMatchProperty::getName, AddonMatchProperty::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 (SddpDevice device : foundDevices) { + logger.trace("Checking device: {}", device.host); + if (propertyMatches(matchProperties, HOST, device.host) + && propertyMatches(matchProperties, IP_ADDRESS, device.ipAddress) + && propertyMatches(matchProperties, MAC_ADDRESS, device.macAddress) + && propertyMatches(matchProperties, MANUFACTURER, device.manufacturer) + && propertyMatches(matchProperties, MODEL, device.model) + && propertyMatches(matchProperties, PORT, device.port) + && propertyMatches(matchProperties, PRIMARY_PROXY, device.primaryProxy) + && propertyMatches(matchProperties, PROXIES, device.proxies) + && propertyMatches(matchProperties, TYPE, device.type)) { + result.add(candidate); + logger.debug("Suggested add-on found: {}", candidate.getUID()); + break; + } + } + } + } + return result; + } + + @Override + public void setAddonCandidates(List candidates) { + super.setAddonCandidates(candidates); + } +} diff --git a/bundles/org.openhab.core.config.discovery.addon.sddp/src/test/java/org/openhab/core/config/discovery/addon/sddp/test/SddpAddonFinderTests.java b/bundles/org.openhab.core.config.discovery.addon.sddp/src/test/java/org/openhab/core/config/discovery/addon/sddp/test/SddpAddonFinderTests.java new file mode 100644 index 000000000..fff72278c --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.addon.sddp/src/test/java/org/openhab/core/config/discovery/addon/sddp/test/SddpAddonFinderTests.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.config.discovery.addon.sddp.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.addon.AddonDiscoveryMethod; +import org.openhab.core.addon.AddonInfo; +import org.openhab.core.addon.AddonMatchProperty; +import org.openhab.core.config.discovery.addon.sddp.SddpAddonFinder; +import org.openhab.core.config.discovery.sddp.SddpDevice; +import org.openhab.core.config.discovery.sddp.SddpDiscoveryService; + +/** + * JUnit tests for the {@link SddpAddonFinder}. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class SddpAddonFinderTests { + + private static final Map DEVICE_FIELDS = Map.of( + // @formatter:off + "From", "\"192.168.4.237:1902\"", + "Host", "\"JVC_PROJECTOR-E0DADC152802\"", + "Max-Age", "1800", + "Type", "\"JVCKENWOOD:Projector\"", + "Primary-Proxy", "\"projector\"", + "Proxies", "\"projector\"", + "Manufacturer", "\"JVCKENWOOD\"", + "Model", "\"DLA-RS3100_NZ8\"", + "Driver", "\"projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i\""); + // @formatter:on + + private List createAddonInfos() { + AddonDiscoveryMethod method = new AddonDiscoveryMethod().setServiceType(SddpAddonFinder.SERVICE_TYPE) + .setMatchProperties(List.of(new AddonMatchProperty("host", "JVC.*"))); + List addonInfos = new ArrayList<>(); + addonInfos.add(AddonInfo.builder("jvc", "binding").withName("JVC").withDescription("JVC Kenwood") + .withDiscoveryMethods(List.of(method)).build()); + return addonInfos; + } + + @Test + public void testFinder() { + SddpDevice device = new SddpDevice(DEVICE_FIELDS, false); + + List addonInfos = createAddonInfos(); + SddpAddonFinder finder = new SddpAddonFinder(mock(SddpDiscoveryService.class)); + + finder.setAddonCandidates(addonInfos); + + Set suggestions; + AddonInfo info; + + finder.deviceAdded(device); + suggestions = finder.getSuggestedAddons(); + assertFalse(suggestions.isEmpty()); + info = suggestions.stream().findFirst().orElse(null); + assertNotNull(info); + assertEquals("JVC Kenwood", info.getDescription()); + + finder.deviceRemoved(device); + suggestions = finder.getSuggestedAddons(); + assertTrue(suggestions.isEmpty()); + } +} 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 index 8486506a8..774f3064b 100644 --- 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 @@ -35,6 +35,10 @@ public class AddonFinderConstants { public static final String CFG_FINDER_MDNS = "suggestionFinderMdns"; public static final String SERVICE_NAME_MDNS = SERVICE_TYPE_MDNS + ADDON_SUGGESTION_FINDER; + public static final String SERVICE_TYPE_SDDP = "sddp"; + public static final String CFG_FINDER_SDDP = "suggestionFinderSddp"; + public static final String SERVICE_NAME_SDDP = SERVICE_TYPE_SDDP + ADDON_SUGGESTION_FINDER; + public static final String SERVICE_TYPE_UPNP = "upnp"; public static final String CFG_FINDER_UPNP = "suggestionFinderUpnp"; public static final String SERVICE_NAME_UPNP = SERVICE_TYPE_UPNP + ADDON_SUGGESTION_FINDER; @@ -43,13 +47,14 @@ public class AddonFinderConstants { public static final String CFG_FINDER_USB = "suggestionFinderUsb"; public static final String SERVICE_NAME_USB = SERVICE_TYPE_USB + ADDON_SUGGESTION_FINDER; - public static final List SUGGESTION_FINDERS = List.of(SERVICE_NAME_IP, SERVICE_NAME_MDNS, SERVICE_NAME_UPNP, - SERVICE_NAME_USB); + public static final List SUGGESTION_FINDERS = List.of(SERVICE_NAME_IP, SERVICE_NAME_MDNS, SERVICE_NAME_SDDP, + SERVICE_NAME_UPNP, SERVICE_NAME_USB); public static final Map SUGGESTION_FINDER_TYPES = Map.of(SERVICE_NAME_IP, SERVICE_TYPE_IP, - SERVICE_NAME_MDNS, SERVICE_TYPE_MDNS, SERVICE_NAME_UPNP, SERVICE_TYPE_UPNP, SERVICE_NAME_USB, - SERVICE_TYPE_USB); + SERVICE_NAME_MDNS, SERVICE_TYPE_MDNS, SERVICE_NAME_SDDP, SERVICE_TYPE_SDDP, SERVICE_NAME_UPNP, + SERVICE_TYPE_UPNP, SERVICE_NAME_USB, SERVICE_TYPE_USB); public static final Map SUGGESTION_FINDER_CONFIGS = Map.of(SERVICE_NAME_IP, CFG_FINDER_IP, - SERVICE_NAME_MDNS, CFG_FINDER_MDNS, SERVICE_NAME_UPNP, CFG_FINDER_UPNP, SERVICE_NAME_USB, CFG_FINDER_USB); + SERVICE_NAME_MDNS, CFG_FINDER_MDNS, SERVICE_NAME_SDDP, CFG_FINDER_SDDP, SERVICE_NAME_UPNP, CFG_FINDER_UPNP, + SERVICE_NAME_USB, CFG_FINDER_USB); } diff --git a/bundles/org.openhab.core.config.discovery.sddp/.classpath b/bundles/org.openhab.core.config.discovery.sddp/.classpath new file mode 100644 index 000000000..585aba264 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.sddp/.classpath @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.config.discovery.sddp/.project b/bundles/org.openhab.core.config.discovery.sddp/.project new file mode 100644 index 000000000..29654a1ee --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.sddp/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.config.discovery.upnp + + + + + + 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.sddp/NOTICE b/bundles/org.openhab.core.config.discovery.sddp/NOTICE new file mode 100644 index 000000000..6c17d0d8a --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.sddp/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.sddp/pom.xml b/bundles/org.openhab.core.config.discovery.sddp/pom.xml new file mode 100644 index 000000000..3e0fc524e --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.sddp/pom.xml @@ -0,0 +1,25 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 4.2.0-SNAPSHOT + + + org.openhab.core.config.discovery.sddp + + openHAB Core :: Bundles :: Configuration SDDP Discovery + + + + org.openhab.core.bundles + org.openhab.core.config.discovery + ${project.version} + + + + diff --git a/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDevice.java b/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDevice.java new file mode 100644 index 000000000..cc7b9d8f2 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDevice.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.config.discovery.sddp; + +import java.time.Instant; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * A DTO class containing data from an SDDP device discovery result. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class SddpDevice { + + public final String from; + public final String host; + public final String maxAge; + public final String type; + public final String primaryProxy; + public final String proxies; + public final String manufacturer; + public final String model; + public final String driver; + public final String ipAddress; + public final String port; + public final String macAddress; + public final Instant expireInstant; + + /** + * Constructor. + * + * @param headers a map of parameter name / value pairs. + * @param offline indicates if the device is being created from a NOTIFY OFFLINE announcement. + */ + public SddpDevice(Map headers, boolean offline) { + from = headers.getOrDefault("From", "").replaceAll("^\"|\"$", ""); + host = headers.getOrDefault("Host", "").replaceAll("^\"|\"$", ""); + maxAge = headers.getOrDefault("Max-Age", "").replaceAll("^\"|\"$", ""); + type = headers.getOrDefault("Type", "").replaceAll("^\"|\"$", ""); + primaryProxy = headers.getOrDefault("Primary-Proxy", "").replaceAll("^\"|\"$", ""); + proxies = headers.getOrDefault("Proxies", "").replaceAll("^\"|\"$", ""); + manufacturer = headers.getOrDefault("Manufacturer", "").replaceAll("^\"|\"$", ""); + model = headers.getOrDefault("Model", "").replaceAll("^\"|\"$", ""); + driver = headers.getOrDefault("Driver", "").replaceAll("^\"|\"$", ""); + + String[] fromParts = from.split(":"); + ipAddress = fromParts[0]; + port = fromParts.length > 1 ? fromParts[1] : ""; + + String[] hostParts = host.split("-|_"); + macAddress = hostParts.length <= 1 ? "" + : hostParts[hostParts.length - 1].replaceAll("(..)(?!$)", "$1-").toLowerCase(); + + expireInstant = offline ? Instant.now().minusMillis(1) + : Instant.now().plusSeconds(maxAge.isBlank() ? 0 : Integer.parseInt(maxAge)); + } + + /** + * Set uniqueness is determined by the From field only + */ + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof SddpDevice other) { + return Objects.equals(from, other.from); + } + return false; + } + + /** + * Set uniqueness is determined by the From field only + */ + @Override + public int hashCode() { + return Objects.hash(from); + } + + /** + * Check if the creation time plus max-age instant is exceeded. + */ + public boolean isExpired() { + return Instant.now().isAfter(expireInstant); + } +} diff --git a/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDeviceParticipant.java b/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDeviceParticipant.java new file mode 100644 index 000000000..e01aa7b56 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDeviceParticipant.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.config.discovery.sddp; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * A {@link SddpDeviceParticipant} that is registered as a service is picked up by the {@link SddpDiscoveryService} and + * can thus be informed when the SDDP service discovers or removes an {@link SddpDevice}. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public interface SddpDeviceParticipant { + + void deviceAdded(SddpDevice device); + + void deviceRemoved(SddpDevice device); +} diff --git a/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDiscoveryParticipant.java b/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDiscoveryParticipant.java new file mode 100644 index 000000000..fd1137550 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDiscoveryParticipant.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.config.discovery.sddp; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; + +/** + * A {@link SddpDiscoveryParticipant} that is registered as a service is picked up by the {@link SddpDiscoveryService} + * and can thus contribute {@link DiscoveryResult}s from SDDP scans. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public interface SddpDiscoveryParticipant { + + /** + * Defines the list of thing types that this participant can identify + * + * @return a set of thing type UIDs for which results can be created + */ + Set getSupportedThingTypeUIDs(); + + /** + * Creates a discovery result for a SDDP device + * + * @param device the SDDP device found on the network + * @return the according discovery result or null, if device is not + * supported by this participant + */ + @Nullable + DiscoveryResult createResult(SddpDevice device); + + /** + * Returns the thing UID for a SDDP device + * + * @param device the SDDP device on the network + * @return a thing UID or null, if device is not supported + * by this participant + */ + @Nullable + ThingUID getThingUID(SddpDevice device); +} diff --git a/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDiscoveryService.java b/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDiscoveryService.java new file mode 100644 index 000000000..f54181b19 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.sddp/src/main/java/org/openhab/core/config/discovery/sddp/SddpDiscoveryService.java @@ -0,0 +1,438 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.config.discovery.sddp; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MulticastSocket; +import java.net.NetworkInterface; +import java.net.ServerSocket; +import java.net.SocketTimeoutException; +import java.net.StandardSocketOptions; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.net.CidrAddress; +import org.openhab.core.net.NetworkAddressChangeListener; +import org.openhab.core.net.NetworkAddressService; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a {@link DiscoveryService} implementation, which can find SDDP devices in the network. + * Support for bindings can be achieved by implementing and registering a {@link SddpDiscoveryParticipant}. + * Support for finders can be achieved by implementing and registering a {@link SddpDeviceParticipant}. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = DiscoveryService.class, property = "protocol=sddp", configurationPid = "discovery.sddp") +public class SddpDiscoveryService extends AbstractDiscoveryService + implements AutoCloseable, NetworkAddressChangeListener { + + private static final int SDDP_PORT = 1902; + private static final String SDDP_IP_ADDRESS = "239.255.255.250"; + private static final InetSocketAddress SDDP_GROUP = new InetSocketAddress(SDDP_IP_ADDRESS, SDDP_PORT); + + private static final int READ_BUFFER_SIZE = 1024; + private static final Duration SOCKET_TIMOUT = Duration.ofMillis(1000); + private static final Duration SEARCH_LISTEN_DURATION = Duration.ofSeconds(5); + private static final Duration CACHE_PURGE_INTERVAL = Duration.ofSeconds(300); + + private static final String SEARCH_REQUEST_BODY_FORMAT = "SEARCH * SDDP/1.0\r\nHost: \"%s:%d\"\r\n"; + private static final String SEARCH_RESPONSE_HEADER = "SDDP/1.0 200 OK"; + + private static final String NOTIFY_ALIVE_HEADER = "NOTIFY ALIVE SDDP/1.0"; + private static final String NOTIFY_OFFLINE_HEADER = "NOTIFY OFFLINE SDDP/1.0"; + + private final Logger logger = LoggerFactory.getLogger(SddpDiscoveryService.class); + private final Set foundDevicesCache = ConcurrentHashMap.newKeySet(); + private final Set discoveryParticipants = ConcurrentHashMap.newKeySet(); + private final Set deviceParticipants = ConcurrentHashMap.newKeySet(); + + private final NetworkAddressService networkAddressService; + + private boolean closing = false; + + private @Nullable Future listenBackgroundMulticastTask = null; + private @Nullable Future listenActiveScanUnicastTask = null; + private @Nullable ScheduledFuture purgeExpiredDevicesTask = null; + + @Activate + public SddpDiscoveryService(final @Nullable Map configProperties, // + final @Reference NetworkAddressService networkAddressService, // + final @Reference TranslationProvider i18nProvider, // + final @Reference LocaleProvider localeProvider) { + super((int) SEARCH_LISTEN_DURATION.getSeconds()); + + this.networkAddressService = networkAddressService; + this.i18nProvider = i18nProvider; + this.localeProvider = localeProvider; + + super.activate(configProperties); // note: this starts listenBackgroundMulticastTask + + purgeExpiredDevicesTask = scheduler.scheduleWithFixedDelay(() -> purgeExpiredDevices(), + CACHE_PURGE_INTERVAL.getSeconds(), CACHE_PURGE_INTERVAL.getSeconds(), TimeUnit.SECONDS); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void addSddpDeviceParticipant(SddpDeviceParticipant participant) { + deviceParticipants.add(participant); + foundDevicesCache.stream().filter(d -> !d.isExpired()).forEach(d -> participant.deviceAdded(d)); + startScan(); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + protected void addSddpDiscoveryParticipant(SddpDiscoveryParticipant participant) { + discoveryParticipants.add(participant); + foundDevicesCache.stream().filter(d -> !d.isExpired()).forEach(d -> { + DiscoveryResult result = participant.createResult(d); + if (result != null) { + DiscoveryResult localizedResult = getLocalizedDiscoveryResult(result, + FrameworkUtil.getBundle(participant.getClass())); + thingDiscovered(localizedResult); + } + }); + } + + /** + * Cancel the given task. + */ + private void cancelTask(@Nullable Future task) { + if (task != null) { + task.cancel(true); + } + } + + @Override + public void close() { + deactivate(); + } + + /** + * Optionally create an {@link SddpDevice) object from UDP packet data if the data is good. + */ + public Optional createSddpDevice(String data) { + if (!data.isBlank()) { + List lines = data.lines().toList(); + if (lines.size() > 1) { + String statement = lines.get(0).strip(); + boolean offline = statement.startsWith(NOTIFY_OFFLINE_HEADER); + if (offline || statement.startsWith(NOTIFY_ALIVE_HEADER) + || statement.startsWith(SEARCH_RESPONSE_HEADER)) { + Map headers = new HashMap<>(); + for (int i = 1; i < lines.size(); i++) { + String[] header = lines.get(i).split(":", 2); + if (header.length > 1) { + headers.put(header[0].strip(), header[1].strip()); + } + } + return Optional.of(new SddpDevice(headers, offline)); + } + } + } + return Optional.empty(); + } + + @Deactivate + @Override + protected void deactivate() { + closing = true; + + foundDevicesCache.clear(); + discoveryParticipants.clear(); + deviceParticipants.clear(); + + super.deactivate(); // note: this cancels and nulls listenBackgroundMulticastTask + + cancelTask(listenActiveScanUnicastTask); + listenActiveScanUnicastTask = null; + + cancelTask(purgeExpiredDevicesTask); + purgeExpiredDevicesTask = null; + } + + @Override + public Set getSupportedThingTypes() { + Set supportedThingTypes = new HashSet<>(); + discoveryParticipants.forEach(p -> supportedThingTypes.addAll(p.getSupportedThingTypeUIDs())); + return supportedThingTypes; + } + + /** + * Continue to listen for incoming SDDP multicast messages until the thread is externally interrupted. + */ + private void listenBackGroundMulticast() { + MulticastSocket socket = null; + NetworkInterface networkInterface = null; + + try { + networkInterface = NetworkInterface + .getByInetAddress(InetAddress.getByName(networkAddressService.getPrimaryIpv4HostAddress())); + + if (logger.isDebugEnabled()) { + logger.debug("listenBackGroundMulticast() starting on interface '{}'", + networkInterface.getDisplayName()); + } + + socket = new MulticastSocket(SDDP_PORT); + socket.joinGroup(SDDP_GROUP, networkInterface); + socket.setSoTimeout((int) SOCKET_TIMOUT.toMillis()); + + DatagramPacket packet = null; + byte[] buffer = new byte[READ_BUFFER_SIZE]; + + // loop listen for responses + while (!Thread.currentThread().isInterrupted()) { + try { + if (packet == null) { + packet = new DatagramPacket(buffer, buffer.length); + } + socket.receive(packet); + processPacket(packet); + packet = null; + } catch (SocketTimeoutException e) { + // socket.receive() will time out every 1 second so the thread won't block + } + } + } catch (IOException e) { + if (!closing) { + logger.warn("listenBackGroundMulticast error '{}'", e.getMessage()); + } + } finally { + if (socket != null && networkInterface != null) { + try { + socket.leaveGroup(SDDP_GROUP, networkInterface); + } catch (IOException e) { + if (!closing) { + logger.warn("listenBackGroundMulticast() error '{}'", e.getMessage()); + } + } + socket.close(); + } + } + } + + /** + * Send a single outgoing SEARCH 'ping' and then continue to listen for incoming SDDP unicast responses until the + * loop time elapses or the thread is externally interrupted. + */ + private void listenActiveScanUnicast() { + // get a free port number + int port; + try (ServerSocket portFinder = new ServerSocket(0)) { + port = portFinder.getLocalPort(); + } catch (IOException e) { + logger.warn("listenActiveScanUnicast() port finder error '{}'", e.getMessage()); + return; + } + + try (DatagramSocket socket = new DatagramSocket(port)) { + String ipAddress = networkAddressService.getPrimaryIpv4HostAddress(); + NetworkInterface networkInterface = NetworkInterface.getByInetAddress(InetAddress.getByName(ipAddress)); + + if (logger.isDebugEnabled()) { + logger.debug("listenActiveScanUnicast() starting on '{}:{}' on interface '{}'", ipAddress, port, + networkInterface.getDisplayName()); + } + + socket.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface); + socket.setSoTimeout((int) SOCKET_TIMOUT.toMillis()); + + DatagramPacket packet; + byte[] buffer; + + // send search request + String search = String.format(SEARCH_REQUEST_BODY_FORMAT, ipAddress, port); + buffer = search.getBytes(StandardCharsets.UTF_8); + packet = new DatagramPacket(buffer, buffer.length, new InetSocketAddress(SDDP_IP_ADDRESS, SDDP_PORT)); + socket.send(packet); + logger.trace("Packet sent to '{}:{}' content:\r\n{}", SDDP_IP_ADDRESS, SDDP_PORT, search); + + final Instant listenDoneTime = Instant.now().plus(SEARCH_LISTEN_DURATION); + buffer = new byte[READ_BUFFER_SIZE]; + packet = null; + + // loop listen for responses + while (Instant.now().isBefore(listenDoneTime) && !Thread.currentThread().isInterrupted()) { + try { + if (packet == null) { + packet = new DatagramPacket(buffer, buffer.length); + } + socket.receive(packet); + processPacket(packet); + packet = null; + } catch (SocketTimeoutException e) { + // receive will time out every 1 second so the thread won't block + } + } + } catch (IOException e) { + if (!closing) { + logger.warn("listenActiveScanUnicast() error '{}'", e.getMessage()); + } + } + } + + @Modified + @Override + protected void modified(@Nullable Map configProperties) { + super.modified(configProperties); + } + + /** + * If the network interfaces change then cancel and recreate all pending tasks. + */ + @Override + public synchronized void onChanged(List added, List removed) { + Future multicastTask = listenBackgroundMulticastTask; + if (multicastTask != null && !multicastTask.isDone()) { + multicastTask.cancel(true); + listenBackgroundMulticastTask = scheduler.submit(() -> listenBackGroundMulticast()); + } + Future unicastTask = listenActiveScanUnicastTask; + if (unicastTask != null && !unicastTask.isDone()) { + unicastTask.cancel(true); + listenActiveScanUnicastTask = scheduler.submit(() -> listenActiveScanUnicast()); + } + } + + /** + * Process the {@link DatagramPacket} content by trying to create an {@link SddpDevice} and eventually adding it to + * the foundDevicesCache, and if so, then notifying all listeners. + * + * @param packet a datagram packet that arrived over the network. + */ + private synchronized void processPacket(DatagramPacket packet) { + String content = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8); + if (logger.isTraceEnabled()) { + logger.trace("Packet received from '{}:{}' content:\r\n{}", packet.getAddress().getHostAddress(), + packet.getPort(), content); + } + Optional deviceOptional = createSddpDevice(content); + if (deviceOptional.isPresent()) { + SddpDevice device = deviceOptional.get(); + foundDevicesCache.remove(device); + + if (device.isExpired()) { + // device created from a NOTIFY OFFLINE announcement + discoveryParticipants.forEach(p -> { + DiscoveryResult discoveryResult = p.createResult(device); + if (discoveryResult != null) { + thingRemoved(discoveryResult.getThingUID()); + } + }); + deviceParticipants.forEach(f -> f.deviceRemoved(device)); + } else { + // device created from a NOTIFY ALIVE announcement or SEARCH response + foundDevicesCache.add(device); + discoveryParticipants.forEach(p -> { + DiscoveryResult discoveryResult = p.createResult(device); + if (discoveryResult != null) { + DiscoveryResult localizedResult = getLocalizedDiscoveryResult(discoveryResult, + FrameworkUtil.getBundle(p.getClass())); + thingDiscovered(localizedResult); + } + }); + deviceParticipants.forEach(f -> f.deviceAdded(device)); + } + + if (logger.isDebugEnabled()) { + logger.debug("processPacket() foundDevices={}, deviceParticipants={}, discoveryParticipants={}", + foundDevicesCache.size(), deviceParticipants.size(), discoveryParticipants.size()); + } + } + } + + /** + * Purge expired devices and notify all listeners. + */ + private synchronized void purgeExpiredDevices() { + Set devices = new HashSet<>(foundDevicesCache); + devices.stream().filter(d -> d.isExpired()).forEach(d -> { + discoveryParticipants.forEach(p -> { + ThingUID thingUID = p.getThingUID(d); + if (thingUID != null) { + thingRemoved(thingUID); + } + }); + deviceParticipants.forEach(f -> f.deviceRemoved(d)); + }); + foundDevicesCache.clear(); + foundDevicesCache.addAll(devices.stream().filter(d -> !d.isExpired()).collect(Collectors.toSet())); + } + + public void removeSddpDeviceParticipant(SddpDeviceParticipant participant) { + deviceParticipants.remove(participant); + } + + public void removeSddpDiscoveryParticipant(SddpDiscoveryParticipant participant) { + discoveryParticipants.remove(participant); + } + + @Override + protected void startBackgroundDiscovery() { + Future task = listenBackgroundMulticastTask; + if (task == null || task.isDone()) { + listenBackgroundMulticastTask = scheduler.submit(() -> listenBackGroundMulticast()); + } + } + + /** + * Schedule to send one single SDDP SEARCH request, and listen for responses. + */ + @Override + protected void startScan() { + Future task = listenActiveScanUnicastTask; + if (task == null || task.isDone()) { + listenActiveScanUnicastTask = scheduler.submit(() -> listenActiveScanUnicast()); + } + } + + @Override + protected void stopBackgroundDiscovery() { + cancelTask(listenBackgroundMulticastTask); + listenBackgroundMulticastTask = null; + } +} diff --git a/bundles/org.openhab.core.config.discovery.sddp/src/test/java/org/openhab/core/config/discovery/sddp/test/SddpDiscoveryServiceTests.java b/bundles/org.openhab.core.config.discovery.sddp/src/test/java/org/openhab/core/config/discovery/sddp/test/SddpDiscoveryServiceTests.java new file mode 100644 index 000000000..ee571e3bd --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.sddp/src/test/java/org/openhab/core/config/discovery/sddp/test/SddpDiscoveryServiceTests.java @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.config.discovery.sddp.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.openhab.core.config.discovery.sddp.SddpDevice; +import org.openhab.core.config.discovery.sddp.SddpDiscoveryService; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.net.NetworkAddressService; + +/** + * JUnit tests for parsing SDDP discovery results. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +@TestInstance(Lifecycle.PER_CLASS) +public class SddpDiscoveryServiceTests { + + private static final String ALIVE_NOTIFICATION = """ + NOTIFY ALIVE SDDP/1.0 + From: "192.168.4.237:1902" + Host: "JVC_PROJECTOR-E0DADC152802" + Max-Age: 1800 + Type: "JVCKENWOOD:Projector" + Primary-Proxy: "projector" + Proxies: "projector" + Manufacturer: "JVCKENWOOD" + Model: "DLA-RS3100_NZ8" + Driver: "projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i" + """; + + private static final String BAD_HEADER = """ + SDDP/1.0 404 NOT FOUND\r + From: "192.168.4.237:1902"\r + Host: "JVC_PROJECTOR-E0DADC152802"\r + Max-Age: 1800\r + Type: "JVCKENWOOD:Projector"\r + Primary-Proxy: "projector"\r + Proxies: "projector"\r + Manufacturer: "JVCKENWOOD"\r + Model: "DLA-RS3100_NZ8"\r + Driver: "projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i"\r + """; + + private static final String BAD_PAYLOAD = """ + SDDP/1.0 200 OK\r + """; + + private static final String SEARCH_RESPONSE = """ + SDDP/1.0 200 OK\r + From: "192.168.4.237:1902"\r + Host: "JVC_PROJECTOR-E0DADC152802"\r + Max-Age: 1800\r + Type: "JVCKENWOOD:Projector"\r + Primary-Proxy: "projector"\r + Proxies: "projector"\r + Manufacturer: "JVCKENWOOD"\r + Model: "DLA-RS3100_NZ8"\r + Driver: "projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i"\r + """; + + private @NonNullByDefault({}) NetworkAddressService networkAddressService; + + @BeforeAll + public void setup() { + networkAddressService = mock(NetworkAddressService.class); + when(networkAddressService.getPrimaryIpv4HostAddress()).thenReturn("192.168.1.1"); + } + + @Test + void testAliveNotification() throws Exception { + try (SddpDiscoveryService service = new SddpDiscoveryService(null, networkAddressService, + mock(TranslationProvider.class), mock(LocaleProvider.class))) { + Optional deviceOptional = service.createSddpDevice(ALIVE_NOTIFICATION); + assertTrue(deviceOptional.isPresent()); + SddpDevice device = deviceOptional.orElse(null); + assertNotNull(device); + assertEquals("192.168.4.237:1902", device.from); + assertEquals("JVC_PROJECTOR-E0DADC152802", device.host); + assertEquals("1800", device.maxAge); + assertEquals("JVCKENWOOD:Projector", device.type); + assertEquals("projector", device.primaryProxy); + assertEquals("projector", device.proxies); + assertEquals("JVCKENWOOD", device.manufacturer); + assertEquals("DLA-RS3100_NZ8", device.model); + assertEquals("projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i", device.driver); + assertEquals("192.168.4.237", device.ipAddress); + assertEquals("e0-da-dc-15-28-02", device.macAddress); + assertEquals("1902", device.port); + } + } + + @Test + void testBadHeader() throws Exception { + try (SddpDiscoveryService service = new SddpDiscoveryService(null, networkAddressService, + mock(TranslationProvider.class), mock(LocaleProvider.class))) { + Optional deviceOptional = service.createSddpDevice(BAD_HEADER); + assertFalse(deviceOptional.isPresent()); + } + } + + @Test + void testBadPayload() throws Exception { + try (SddpDiscoveryService service = new SddpDiscoveryService(null, networkAddressService, + mock(TranslationProvider.class), mock(LocaleProvider.class))) { + Optional deviceOptional = service.createSddpDevice(BAD_PAYLOAD); + assertFalse(deviceOptional.isPresent()); + } + } + + @Test + void testSearchResponse() throws Exception { + try (SddpDiscoveryService service = new SddpDiscoveryService(null, networkAddressService, + mock(TranslationProvider.class), mock(LocaleProvider.class))) { + Optional deviceOptional = service.createSddpDevice(SEARCH_RESPONSE); + assertTrue(deviceOptional.isPresent()); + SddpDevice device = deviceOptional.orElse(null); + assertNotNull(device); + assertEquals("192.168.4.237:1902", device.from); + assertEquals("JVC_PROJECTOR-E0DADC152802", device.host); + assertEquals("1800", device.maxAge); + assertEquals("JVCKENWOOD:Projector", device.type); + assertEquals("projector", device.primaryProxy); + assertEquals("projector", device.proxies); + assertEquals("JVCKENWOOD", device.manufacturer); + assertEquals("DLA-RS3100_NZ8", device.model); + assertEquals("projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i", device.driver); + assertEquals("192.168.4.237", device.ipAddress); + assertEquals("e0-da-dc-15-28-02", device.macAddress); + assertEquals("1902", device.port); + } + } +} 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 752351223..ae137724f 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 @@ -34,6 +34,12 @@ Use IP network discovery broadcasts to suggest add-ons. Enabling/disabling may take up to 1 minute. + + true + + Use SDDP network scan to suggest add-ons. Enabling/disabling may take up to 1 minute. + true + true diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons.properties b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons.properties index 0d22f9d7d..88e513f26 100644 --- a/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons.properties +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/i18n/addons.properties @@ -6,6 +6,8 @@ system.config.addons.suggestionFinderIp.label = IP-based Suggestion Finder system.config.addons.suggestionFinderIp.description = Use IP network discovery broadcasts to suggest add-ons. Enabling/disabling may take up to 1 minute. system.config.addons.suggestionFinderMdns.label = mDNS Suggestion Finder system.config.addons.suggestionFinderMdns.description = Use mDNS network scan to suggest add-ons. Enabling/disabling may take up to 1 minute. +system.config.addons.suggestionFinderSddp.label = SDDP Suggestion Finder +system.config.addons.suggestionFinderSddp.description = Use SDDP network scan to suggest add-ons. Enabling/disabling may take up to 1 minute. system.config.addons.suggestionFinderUpnp.label = UPnP Suggestion Finder system.config.addons.suggestionFinderUpnp.description = Use UPnP network scan to suggest add-ons. Enabling/disabling may take up to 1 minute. system.config.addons.suggestionFinderUsb.label = USB Suggestion Finder diff --git a/bundles/pom.xml b/bundles/pom.xml index d83958afe..25918c2b2 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -34,9 +34,11 @@ org.openhab.core.config.discovery.addon.ip org.openhab.core.config.discovery.addon.mdns org.openhab.core.config.discovery.addon.process + org.openhab.core.config.discovery.addon.sddp org.openhab.core.config.discovery.addon.upnp org.openhab.core.config.discovery.addon.usb org.openhab.core.config.discovery.mdns + org.openhab.core.config.discovery.sddp org.openhab.core.config.discovery.usbserial org.openhab.core.config.discovery.usbserial.linuxsysfs org.openhab.core.config.discovery.usbserial.ser2net diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 9f55be0cd..2305b15a2 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -105,6 +105,18 @@ mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon.usb/${project.version} + + openhab-core-base + openhab-core-config-discovery-addon + openhab-core-config-discovery-sddp + mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.addon.sddp/${project.version} + + + + openhab-core-base + mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.sddp/${project.version} + + kar openhab-core-base