]> git.basschouten.com Git - openhab-addons.git/commitdiff
[atlona] Replace discovery service with core SDDP discovery (#16832)
authormlobstein <michael.lobstein@gmail.com>
Fri, 7 Jun 2024 16:54:14 +0000 (11:54 -0500)
committerGitHub <noreply@github.com>
Fri, 7 Jun 2024 16:54:14 +0000 (18:54 +0200)
* Replace discovery service with core SDDP discovery

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
bundles/org.openhab.binding.atlona/README.md
bundles/org.openhab.binding.atlona/src/main/feature/feature.xml
bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscovery.java [deleted file]
bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscoveryParticipant.java [new file with mode: 0644]
bundles/org.openhab.binding.atlona/src/main/resources/OH-INF/addon/addon.xml

index 121563ebe0c4f94382a96b177176071ba4445b24..9330fbb3e4e6dc90a1d506478d385512386df49a 100644 (file)
@@ -17,9 +17,9 @@ This binding supports the following thing types:
 
 ## Discovery
 
-The Atlona AT-UHD-PRO3 switch can be discovered by starting a discovery scan in the UI and then logging into your switch and pressing the "SDDP" button on the "Network" tab.
-The "SDDP" (simple device discovery protocol) button will initiate the discovery process.
-If "Telnet Login" is enabled ("Network" tab from the switch configuration UI), you will need to set the username and password in the configuration of the newly discovered thing before a connection can be made.
+Supported things should be discovered automatically upon receipt of periodic SDDP announcements from the switch. 
+If the thing is not discovered automatically, login to the switch configuration UI and press the "SDDP" button on the "Network" tab to force the switch to send the SDDP announcement.
+If "Telnet Login" is enabled in the switch configuration, you will need to set the username and password in the newly discovered thing before a connection can be made.
 
 ## Thing Configuration
 
index 729fcb94d642855684d8f3b7d37054ab05f919c8..ea775b169e5870c6d7dabf8d7b8660e000fb27ea 100644 (file)
@@ -4,6 +4,7 @@
 
        <feature name="openhab-binding-atlona" description="Atlona Binding" version="${project.version}">
                <feature>openhab-runtime-base</feature>
+               <feature>openhab-core-config-discovery-sddp</feature>
                <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.atlona/${project.version}</bundle>
        </feature>
 </features>
diff --git a/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscovery.java b/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscovery.java
deleted file mode 100644 (file)
index 3e9c806..0000000
+++ /dev/null
@@ -1,273 +0,0 @@
-/**
- * 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.binding.atlona.internal.discovery;
-
-import static org.openhab.binding.atlona.internal.AtlonaBindingConstants.*;
-
-import java.io.IOException;
-import java.net.DatagramPacket;
-import java.net.InetAddress;
-import java.net.MulticastSocket;
-import java.net.NetworkInterface;
-import java.net.SocketTimeoutException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import org.openhab.binding.atlona.internal.pro3.AtlonaPro3Config;
-import org.openhab.core.config.discovery.AbstractDiscoveryService;
-import org.openhab.core.config.discovery.DiscoveryResult;
-import org.openhab.core.config.discovery.DiscoveryResultBuilder;
-import org.openhab.core.config.discovery.DiscoveryService;
-import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.thing.ThingUID;
-import org.osgi.service.component.annotations.Component;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Discovery class for the Atlona PRO3 line. The PRO3 line uses SDDP (simple device discovery protocol) for discovery
- * (similar to UPNP but defined by Control4). The user should start the discovery process in openhab and then log into
- * the switch, go to the Network options and press the SDDP button (which initiates the SDDP conversation).
- *
- * @author Tim Roberts - Initial contribution
- */
-@Component(service = DiscoveryService.class, configurationPid = "discovery.atlona")
-public class AtlonaDiscovery extends AbstractDiscoveryService {
-
-    private final Logger logger = LoggerFactory.getLogger(AtlonaDiscovery.class);
-
-    /**
-     * Address SDDP broadcasts on
-     */
-    private static final String SDDP_ADDR = "239.255.255.250";
-
-    /**
-     * Port number SDDP uses
-     */
-    private static final int SDDP_PORT = 1902;
-
-    /**
-     * SDDP packet should be only 512 in size - make it 600 to give us some room
-     */
-    private static final int BUFFER_SIZE = 600;
-
-    /**
-     * Socket read timeout (in ms) - allows us to shutdown the listening every TIMEOUT
-     */
-    private static final int TIMEOUT = 1000;
-
-    /**
-     * Whether we are currently scanning or not
-     */
-    private boolean scanning;
-
-    /**
-     * The {@link ExecutorService} to run the listening threads on.
-     */
-    private ExecutorService executorService;
-
-    /**
-     * Constructs the discovery class using the thing IDs that we can discover.
-     */
-    public AtlonaDiscovery() {
-        super(Collections.unmodifiableSet(
-                Stream.of(THING_TYPE_PRO3_44M, THING_TYPE_PRO3_66M, THING_TYPE_PRO3_88M, THING_TYPE_PRO3_1616M)
-                        .collect(Collectors.toSet())),
-                30, false);
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * Starts the scan. This discovery will:
-     * <ul>
-     * <li>Request all the network interfaces</li>
-     * <li>For each network interface, create a listening thread using {@link #executorService}</li>
-     * <li>Each listening thread will open up a {@link MulticastSocket} using {@link #SDDP_ADDR} and {@link #SDDP_PORT}
-     * and
-     * will receive any {@link DatagramPacket} that comes in</li>
-     * <li>The {@link DatagramPacket} is then investigated to see if is a SDDP packet and will create a new thing from
-     * it</li>
-     * </ul>
-     * The process will continue until {@link #stopScan()} is called.
-     */
-    @Override
-    protected void startScan() {
-        if (executorService != null) {
-            stopScan();
-        }
-
-        logger.debug("Starting Discovery");
-
-        try {
-            final InetAddress addr = InetAddress.getByName(SDDP_ADDR);
-            final List<NetworkInterface> networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
-
-            executorService = Executors.newFixedThreadPool(networkInterfaces.size());
-            scanning = true;
-            for (final NetworkInterface netint : networkInterfaces) {
-
-                executorService.execute(() -> {
-                    try {
-                        MulticastSocket multiSocket = new MulticastSocket(SDDP_PORT);
-                        multiSocket.setSoTimeout(TIMEOUT);
-                        multiSocket.setNetworkInterface(netint);
-                        multiSocket.joinGroup(addr);
-
-                        while (scanning) {
-                            DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE);
-                            try {
-                                multiSocket.receive(receivePacket);
-
-                                String message = new String(receivePacket.getData()).trim();
-                                if (message.length() > 0) {
-                                    messageReceive(message);
-                                }
-                            } catch (SocketTimeoutException e) {
-                                // ignore
-                            }
-                        }
-
-                        multiSocket.close();
-                    } catch (Exception e) {
-                        if (!e.getMessage().contains("No IP addresses bound to interface")) {
-                            logger.debug("Error getting ip addresses: {}", e.getMessage(), e);
-                        }
-                    }
-                });
-            }
-        } catch (IOException e) {
-            logger.debug("Error getting ip addresses: {}", e.getMessage(), e);
-        }
-    }
-
-    /**
-     * SDDP message has the following format
-     *
-     * <pre>
-     * NOTIFY ALIVE SDDP/1.0
-     * From: "192.168.1.30:1902"
-     * Host: "AT-UHD-PRO3-88M_B898B0030F4D"
-     * Type: "AT-UHD-PRO3-88M"
-     * Max-Age: 1800
-     * Primary-Proxy: "avswitch"
-     * Proxies: "avswitch"
-     * Manufacturer: "Atlona"
-     * Model: "AT-UHD-PRO3-88M"
-     * Driver: "avswitch_Atlona_AT-UHD-PRO3-88M_IP.c4i"
-     * Config-URL: "http://192.168.1.30/"
-     * </pre>
-     *
-     * First parse the manufacturer, host, model and IP address from the message. For the "Host" field, we parse out the
-     * serial #. For the From field, we parse out the IP address (minus the port #). If we successfully found all four
-     * and the manufacturer is "Atlona" and it's a model we recognize, we then create our thing from it.
-     *
-     * @param message possibly null, possibly empty SDDP message
-     */
-    private void messageReceive(String message) {
-        if (message == null || message.trim().length() == 0) {
-            return;
-        }
-
-        String host = null;
-        String model = null;
-        String from = null;
-        String manufacturer = null;
-
-        for (String msg : message.split("\r\n")) {
-            int idx = msg.indexOf(':');
-            if (idx > 0) {
-                String name = msg.substring(0, idx);
-
-                if ("Host".equalsIgnoreCase(name)) {
-                    host = msg.substring(idx + 1).trim().replace("\"", "");
-                    int sep = host.indexOf('_');
-                    if (sep >= 0) {
-                        host = host.substring(sep + 1);
-                    }
-                } else if ("Model".equalsIgnoreCase(name)) {
-                    model = msg.substring(idx + 1).trim().replace("\"", "");
-                } else if ("Manufacturer".equalsIgnoreCase(name)) {
-                    manufacturer = msg.substring(idx + 1).trim().replace("\"", "");
-                } else if ("From".equalsIgnoreCase(name)) {
-                    from = msg.substring(idx + 1).trim().replace("\"", "");
-                    int sep = from.indexOf(':');
-                    if (sep >= 0) {
-                        from = from.substring(0, sep);
-                    }
-                }
-            }
-
-        }
-
-        if (!"Atlona".equalsIgnoreCase(manufacturer)) {
-            return;
-        }
-
-        if (host != null && model != null && from != null) {
-            ThingTypeUID typeId = null;
-            if ("AT-UHD-PRO3-44M".equalsIgnoreCase(model)) {
-                typeId = THING_TYPE_PRO3_44M;
-            } else if ("AT-UHD-PRO3-66M".equalsIgnoreCase(model)) {
-                typeId = THING_TYPE_PRO3_66M;
-            } else if ("AT-UHD-PRO3-88M".equalsIgnoreCase(model)) {
-                typeId = THING_TYPE_PRO3_88M;
-            } else if ("AT-UHD-PRO3-1616M".equalsIgnoreCase(model)) {
-                typeId = THING_TYPE_PRO3_1616M;
-            } else {
-                logger.warn("Unknown model #: {}", model);
-            }
-
-            if (typeId != null) {
-                logger.debug("Creating binding for {} ({})", model, from);
-                ThingUID j = new ThingUID(typeId, host);
-
-                Map<String, Object> properties = new HashMap<>(1);
-                properties.put(AtlonaPro3Config.IP_ADDRESS, from);
-                DiscoveryResult result = DiscoveryResultBuilder.create(j).withProperties(properties)
-                        .withLabel(model + " (" + from + ")").build();
-                thingDiscovered(result);
-            }
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * Stops the discovery scan. We set {@link #scanning} to false (allowing the listening threads to end naturally
-     * within {@link #TIMEOUT} * 5 time then shutdown the {@link ExecutorService}
-     */
-    @Override
-    protected synchronized void stopScan() {
-        super.stopScan();
-        if (executorService == null) {
-            return;
-        }
-
-        scanning = false;
-
-        try {
-            executorService.awaitTermination(TIMEOUT * 5, TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-        }
-        executorService.shutdown();
-        executorService = null;
-    }
-}
diff --git a/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscoveryParticipant.java b/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscoveryParticipant.java
new file mode 100644 (file)
index 0000000..144d6f8
--- /dev/null
@@ -0,0 +1,130 @@
+/**
+ * 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.binding.atlona.internal.discovery;
+
+import static org.openhab.binding.atlona.internal.AtlonaBindingConstants.*;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.atlona.internal.pro3.AtlonaPro3Config;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.sddp.SddpDevice;
+import org.openhab.core.config.discovery.sddp.SddpDiscoveryParticipant;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * Discovery Service for Atlona HDMI matrices that support SDDP.
+ *
+ * @author Michael Lobstein - Initial contribution
+ *
+ */
+@NonNullByDefault
+@Component(immediate = true)
+public class AtlonaDiscoveryParticipant implements SddpDiscoveryParticipant {
+    private final Logger logger = LoggerFactory.getLogger(AtlonaDiscoveryParticipant.class);
+
+    private static final String ATLONA = "ATLONA";
+    private static final String PROXY_AVSWITCH = "avswitch";
+
+    @Override
+    public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
+        return Set.of(THING_TYPE_PRO3_44M, THING_TYPE_PRO3_66M, THING_TYPE_PRO3_88M, THING_TYPE_PRO3_1616M,
+                THING_TYPE_PRO3HD_44M, THING_TYPE_PRO3HD_66M);
+    }
+
+    @Override
+    public @Nullable DiscoveryResult createResult(SddpDevice device) {
+        final ThingUID uid = getThingUID(device);
+        if (uid != null) {
+            final Map<String, Object> properties = new HashMap<>(2);
+            final String label = device.model + " (" + device.ipAddress + ")";
+
+            properties.put(Thing.PROPERTY_MAC_ADDRESS, device.macAddress);
+            properties.put(AtlonaPro3Config.IP_ADDRESS, device.ipAddress);
+
+            final DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
+                    .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).withLabel(label).build();
+
+            logger.debug("Created a DiscoveryResult for device '{}' with UID '{}'", label, uid.getId());
+            return result;
+        } else {
+            return null;
+        }
+    }
+
+    /*
+     * The Atlona SDDP message has the following format
+     *
+     * <pre>
+     * NOTIFY ALIVE SDDP/1.0
+     * From: "192.168.1.30:1902"
+     * Host: "AT-UHD-PRO3-88M_B898B0030F4D"
+     * Type: "AT-UHD-PRO3-88M"
+     * Max-Age: 1800
+     * Primary-Proxy: "avswitch"
+     * Proxies: "avswitch"
+     * Manufacturer: "Atlona"
+     * Model: "AT-UHD-PRO3-88M"
+     * Driver: "avswitch_Atlona_AT-UHD-PRO3-88M_IP.c4i"
+     * Config-URL: "http://192.168.1.30/"
+     * </pre>
+     */
+    @Override
+    public @Nullable ThingUID getThingUID(SddpDevice device) {
+        if (device.manufacturer.toUpperCase(Locale.ENGLISH).contains(ATLONA)
+                && PROXY_AVSWITCH.equals(device.primaryProxy) && !device.macAddress.isBlank()
+                && !device.ipAddress.isBlank()) {
+            final ThingTypeUID typeId;
+
+            switch (device.model) {
+                case "AT-UHD-PRO3-44M":
+                    typeId = THING_TYPE_PRO3_44M;
+                    break;
+                case "AT-UHD-PRO3-66M":
+                    typeId = THING_TYPE_PRO3_66M;
+                    break;
+                case "AT-UHD-PRO3-88M":
+                    typeId = THING_TYPE_PRO3_88M;
+                    break;
+                case "AT-UHD-PRO3-1616M":
+                    typeId = THING_TYPE_PRO3_1616M;
+                    break;
+                case "AT-PRO3HD44M":
+                    typeId = THING_TYPE_PRO3HD_44M;
+                    break;
+                case "AT-PRO3HD66M":
+                    typeId = THING_TYPE_PRO3HD_66M;
+                    break;
+                default:
+                    logger.warn("Unknown model #: {}", device.model);
+                    return null;
+            }
+
+            logger.debug("Atlona matrix with mac {} found at {}", device.macAddress, device.ipAddress);
+            return new ThingUID(typeId, device.macAddress);
+        }
+        return null;
+    }
+}
index 491c7b8940f1adc63737c549b9ccbbade58afc97..ff8c45788740a944dc4ac7048c981873c214b8d2 100644 (file)
@@ -8,4 +8,20 @@
        <description>Binding for Atlona PRO3 HDBaseT Matrix switches.</description>
        <connection>local</connection>
 
+       <discovery-methods>
+               <discovery-method>
+                       <service-type>sddp</service-type>
+                       <match-properties>
+                               <match-property>
+                                       <name>manufacturer</name>
+                                       <regex>(?i).*atlona.*</regex>
+                               </match-property>
+                               <match-property>
+                                       <name>model</name>
+                                       <regex>(?i).*(AT-UHD-PRO3|AT-PRO3HD).*</regex>
+                               </match-property>
+                       </match-properties>
+               </discovery-method>
+       </discovery-methods>
+
 </addon:addon>