]> git.basschouten.com Git - openhab-addons.git/commitdiff
Improve discovery service (#11258)
authormlobstein <michael.lobstein@gmail.com>
Sat, 18 Sep 2021 11:51:30 +0000 (06:51 -0500)
committerGitHub <noreply@github.com>
Sat, 18 Sep 2021 11:51:30 +0000 (13:51 +0200)
Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
bundles/org.openhab.binding.kaleidescape/README.md
bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryJob.java [deleted file]
bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryService.java

index b92a7f9b10ac2106422c52897181106b172dd17e..29b00a059b7f6c43bf201751024a3843025559df 100644 (file)
@@ -32,9 +32,8 @@ The supported thing types are:
 
 ## Discovery
 
-Manually initiated Auto-discovery is supported if Kaleidescape components are accessible on the same IP subnet of the openHAB server.
-Since discovery involves scanning all IP addresses in the subnet range for an open socket, the discovery must be initiated by the user.
-In the Inbox, select Search For Things and then choose the Kaleidescape System Binding to initiate discovery.
+Manually initiated Auto-discovery will locate all suported Kaleidescape components if they are on the same IP subnet of the openHAB server.
+In the Inbox, select Search For Things and then choose the Kaleidescape System Binding to initiate a discovery scan.
 
 ## Binding Configuration
 
diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryJob.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryJob.java
deleted file mode 100644 (file)
index f9f5b61..0000000
+++ /dev/null
@@ -1,209 +0,0 @@
-/**
- * Copyright (c) 2010-2021 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.kaleidescape.internal.discovery;
-
-import static org.openhab.binding.kaleidescape.internal.KaleidescapeBindingConstants.*;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.PrintWriter;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.net.UnknownHostException;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.core.thing.ThingTypeUID;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link KaleidescapeDiscoveryJob} class allow manual discovery of
- * Kaleidescape components for a single IP address. This is used
- * for threading to make discovery faster.
- *
- * @author Chris Graham - Initial contribution
- * @author Michael Lobstein - Adapted for the Kaleidescape binding
- * 
- */
-@NonNullByDefault
-public class KaleidescapeDiscoveryJob implements Runnable {
-    private final Logger logger = LoggerFactory.getLogger(KaleidescapeDiscoveryJob.class);
-
-    // Component Types
-    private static final String PLAYER = "Player";
-    private static final String CINEMA_ONE = "Cinema One";
-    private static final String ALTO = "Alto";
-    private static final String STRATO = "Strato";
-    private static final String STRATO_S = "Strato S";
-    private static final String DISC_VAULT = "Disc Vault";
-
-    private static final Set<String> ALLOWED_DEVICES = new HashSet<String>(
-            Arrays.asList(PLAYER, CINEMA_ONE, ALTO, STRATO, STRATO_S, DISC_VAULT));
-
-    private KaleidescapeDiscoveryService discoveryClass;
-
-    private ThingTypeUID thingTypeUid = THING_TYPE_PLAYER;
-    private String ipAddress = EMPTY;
-    private String friendlyName = EMPTY;
-    private String serialNumber = EMPTY;
-
-    public KaleidescapeDiscoveryJob(KaleidescapeDiscoveryService service, String ip) {
-        this.discoveryClass = service;
-        this.ipAddress = ip;
-    }
-
-    @Override
-    public void run() {
-        if (hasKaleidescapeDevice(this.ipAddress)) {
-            discoveryClass.submitDiscoveryResults(this.thingTypeUid, this.ipAddress, this.friendlyName,
-                    this.serialNumber);
-        }
-    }
-
-    /**
-     * Determines if a Kaleidescape component with a movie player zone is available at a given IP address.
-     *
-     * @param ip IP address of the Kaleidescape component as a string.
-     * @return True if a component is found, false if not.
-     */
-    private boolean hasKaleidescapeDevice(String ip) {
-        try {
-            InetAddress address = InetAddress.getByName(ip);
-
-            if (isKaleidescapeDevice(address, DEFAULT_API_PORT)) {
-                return true;
-            } else {
-                logger.debug("No Kaleidescape component found at IP address ({})", ip);
-                return false;
-            }
-        } catch (UnknownHostException e) {
-            logger.debug("Unknown host: {} - {}", ip, e.getMessage());
-            return false;
-        }
-    }
-
-    /**
-     * Tries to establish a connection to a hostname and port and then interrogate the component
-     *
-     * @param host Hostname or IP address to connect to.
-     * @param port Port to attempt to connect to.
-     * @return True if the component found is one the binding supports
-     */
-    private boolean isKaleidescapeDevice(InetAddress host, int port) {
-        try (Socket socket = new Socket()) {
-            socket.connect(new InetSocketAddress(host, port), DISCOVERY_DEFAULT_IP_TIMEOUT_RATE_MS);
-
-            OutputStream output = socket.getOutputStream();
-            PrintWriter writer = new PrintWriter(output, true);
-
-            // query the component to see if it has video zones, the device type, friendly name, and serial number
-            writer.println("01/1/GET_NUM_ZONES:");
-            writer.println("01/1/GET_DEVICE_TYPE_NAME:");
-            writer.println("01/1/GET_FRIENDLY_NAME:");
-            writer.println("01/1/GET_DEVICE_INFO:");
-
-            InputStream input = socket.getInputStream();
-
-            BufferedReader reader = new BufferedReader(new InputStreamReader(input));
-
-            String componentType = EMPTY;
-            String line;
-            String videoZone = null;
-            String audioZone = null;
-            int lineCount = 0;
-
-            while ((line = reader.readLine()) != null) {
-                String[] strArr = line.split(":");
-
-                if (strArr.length >= 4) {
-                    switch (strArr[1]) {
-                        case "NUM_ZONES":
-                            videoZone = strArr[2];
-                            audioZone = strArr[3];
-                            break;
-                        case "DEVICE_TYPE_NAME":
-                            componentType = strArr[2];
-                            break;
-                        case "FRIENDLY_NAME":
-                            friendlyName = strArr[2];
-                            break;
-                        case "DEVICE_INFO":
-                            serialNumber = strArr[3].trim(); // take off leading zeros
-                            break;
-                    }
-                } else {
-                    logger.debug("isKaleidescapeDevice() - Unable to process line: {}", line);
-                }
-
-                lineCount++;
-
-                // stop after reading four lines
-                if (lineCount > 3) {
-                    break;
-                }
-            }
-
-            // see if we have a video zone
-            if ("01".equals(videoZone)) {
-                // now check if we are one of the allowed types
-                if (ALLOWED_DEVICES.contains(componentType)) {
-                    if (STRATO_S.equals(componentType) || STRATO.equals(componentType)) {
-                        thingTypeUid = THING_TYPE_STRATO;
-                        return true;
-                    }
-
-                    // A 'Player' without an audio zone is really a Strato C
-                    // does not work yet, Strato C erroneously reports "01" for audio zones
-                    // so we are unable to differentiate a Strato C from a Premiere player
-                    if ("00".equals(audioZone) && PLAYER.equals(componentType)) {
-                        thingTypeUid = THING_TYPE_STRATO;
-                        return true;
-                    }
-
-                    // Alto
-                    if (ALTO.equals(componentType)) {
-                        thingTypeUid = THING_TYPE_ALTO;
-                        return true;
-                    }
-
-                    // Cinema One
-                    if (CINEMA_ONE.equals(componentType)) {
-                        thingTypeUid = THING_TYPE_CINEMA_ONE;
-                        return true;
-                    }
-
-                    // A Disc Vault with a video zone (the M700 vault), just call it a THING_TYPE_PLAYER
-                    if (DISC_VAULT.equals(componentType)) {
-                        thingTypeUid = THING_TYPE_PLAYER;
-                        return true;
-                    }
-
-                    // default returns THING_TYPE_PLAYER
-                    return true;
-                }
-            }
-        } catch (IOException e) {
-            logger.debug("isKaleidescapeDevice() IOException: {}", e.getMessage());
-            return false;
-        }
-
-        return false;
-    }
-}
index 2505e7fd4fefe3a98ad693cf9c681d2a43afed75..8bb2388bbc850901f8d04dee05271fdedd8c719e 100644 (file)
@@ -14,26 +14,30 @@ package org.openhab.binding.kaleidescape.internal.discovery;
 
 import static org.openhab.binding.kaleidescape.internal.KaleidescapeBindingConstants.*;
 
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.InterfaceAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.util.ArrayList;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
+import java.util.HashSet;
 import java.util.Set;
 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.apache.commons.net.util.SubnetUtils;
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.core.common.NamedThreadFactory;
 import org.openhab.core.config.discovery.AbstractDiscoveryService;
 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
@@ -50,7 +54,7 @@ import org.slf4j.LoggerFactory;
  *
  * @author Chris Graham - Initial contribution
  * @author Michael Lobstein - Adapted for the Kaleidescape binding
- * 
+ *
  */
 @NonNullByDefault
 @Component(service = DiscoveryService.class, configurationPid = "discovery.kaleidescape")
@@ -60,6 +64,29 @@ public class KaleidescapeDiscoveryService extends AbstractDiscoveryService {
             .unmodifiableSet(Stream.of(THING_TYPE_PLAYER, THING_TYPE_CINEMA_ONE, THING_TYPE_ALTO, THING_TYPE_STRATO)
                     .collect(Collectors.toSet()));
 
+    private static final int K_HEARTBEAT_PORT = 1443;
+
+    // Component Types
+    private static final String PLAYER = "Player";
+    private static final String CINEMA_ONE = "Cinema One";
+    private static final String ALTO = "Alto";
+    private static final String STRATO = "Strato";
+    private static final String STRATO_S = "Strato S";
+    private static final String DISC_VAULT = "Disc Vault";
+
+    private static final Set<String> ALLOWED_DEVICES = new HashSet<String>(
+            Arrays.asList(PLAYER, CINEMA_ONE, ALTO, STRATO, STRATO_S, DISC_VAULT));
+
+    @Nullable
+    private ExecutorService executorService = null;
+
+    /**
+     * Whether we are currently scanning or not
+     */
+    private boolean scanning;
+
+    private Set<String> foundIPs = new HashSet<String>();
+
     @Activate
     public KaleidescapeDiscoveryService() {
         super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_DEFAULT_TIMEOUT_RATE_MS, DISCOVERY_DEFAULT_AUTO_DISCOVER);
@@ -70,83 +97,214 @@ public class KaleidescapeDiscoveryService extends AbstractDiscoveryService {
         return SUPPORTED_THING_TYPES_UIDS;
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * Starts the scan. This discovery will:
+     * <ul>
+     * <li>Create a listening thread that opens up a broadcast {@link DatagramSocket} on port {@link #K_HEARTBEAT_PORT}
+     * and will receive any {@link DatagramPacket} that comes in</li>
+     * <li>The source IP address of the {@link DatagramPacket} is interrogated to verify it is a Kaleidescape component
+     * and will create a new thing from it</li>
+     * </ul>
+     * The process will continue until {@link #stopScan()} is called.
+     */
     @Override
     protected void startScan() {
         logger.debug("Starting discovery of Kaleidescape components.");
 
-        try {
-            List<String> ipList = getIpAddressScanList();
+        if (executorService != null) {
+            stopScan();
+        }
 
-            ExecutorService discoverySearchPool = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE,
-                    new NamedThreadFactory("OH-binding-discovery.kaleidescape", true));
+        final ExecutorService service = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE,
+                new NamedThreadFactory("OH-binding-discovery.kaleidescape", true));
+        executorService = service;
 
-            for (String ip : ipList) {
-                discoverySearchPool.execute(new KaleidescapeDiscoveryJob(this, ip));
-            }
+        scanning = true;
+        foundIPs.clear();
 
-            discoverySearchPool.shutdown();
-        } catch (Exception exp) {
-            logger.debug("Kaleidescape discovery service encountered an error while scanning for components: {}",
-                    exp.getMessage());
-        }
+        service.execute(() -> {
+            try {
+                DatagramSocket dSocket = new DatagramSocket(K_HEARTBEAT_PORT);
+                dSocket.setSoTimeout(DISCOVERY_DEFAULT_TIMEOUT_RATE_MS);
+                dSocket.setBroadcast(true);
 
-        logger.debug("Completed discovery of Kaleidescape components.");
+                while (scanning) {
+                    DatagramPacket receivePacket = new DatagramPacket(new byte[1], 1);
+                    try {
+                        dSocket.receive(receivePacket);
+
+                        if (!foundIPs.contains(receivePacket.getAddress().getHostAddress())) {
+                            String foundIp = receivePacket.getAddress().getHostAddress();
+                            logger.debug("RECEIVED Kaleidescape packet from: {}", foundIp);
+                            foundIPs.add(foundIp);
+                            isKaleidescapeDevice(foundIp);
+                        }
+                    } catch (SocketTimeoutException e) {
+                        // ignore
+                        continue;
+                    }
+                }
+
+                dSocket.close();
+            } catch (IOException e) {
+                logger.debug("KaleidescapeDiscoveryService IOException: {}", e.getMessage(), e);
+            }
+        });
     }
 
     /**
-     * Create a new Thing with an IP address and Component type given. Uses default port.
+     * {@inheritDoc}
      *
-     * @param thingTypeUid ThingTypeUID of detected Kaleidescape component.
-     * @param ip IP address of the Kaleidescape component as a string.
-     * @param friendlyName Name of Kaleidescape component as a string.
-     * @param serialNumber Serial Number of Kaleidescape component as a string.
+     * Stops the discovery scan. We set {@link #scanning} to false (allowing the listening thread to end naturally
+     * within {@link #TIMEOUT) * 5 time then shutdown the {@link #executorService}
      */
-    public void submitDiscoveryResults(ThingTypeUID thingTypeUid, String ip, String friendlyName, String serialNumber) {
-        ThingUID uid = new ThingUID(thingTypeUid, serialNumber);
-
-        HashMap<String, Object> properties = new HashMap<>();
+    @Override
+    protected synchronized void stopScan() {
+        super.stopScan();
+        ExecutorService service = executorService;
+        if (service == null) {
+            return;
+        }
 
-        properties.put("host", ip);
-        properties.put("port", DEFAULT_API_PORT);
+        scanning = false;
 
-        thingDiscovered(DiscoveryResultBuilder.create(uid).withProperties(properties).withRepresentationProperty("host")
-                .withLabel(friendlyName).build());
+        try {
+            service.awaitTermination(DISCOVERY_DEFAULT_TIMEOUT_RATE_MS * 5, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+        }
+        service.shutdown();
+        executorService = null;
     }
 
     /**
-     * Provide a string list of all the IP addresses associated with the network interfaces on
-     * this machine.
+     * Tries to establish a connection to the specified ip address and then interrogate the component,
+     * creates a discovery result if a valid component is found.
      *
-     * @return String list of IP addresses.
-     * @throws UnknownHostException
-     * @throws SocketException
+     * @param ipAddress IP address to connect to
      */
-    private List<String> getIpAddressScanList() throws UnknownHostException, SocketException {
-        List<String> results = new ArrayList<>();
+    private void isKaleidescapeDevice(String ipAddress) {
+        try (Socket socket = new Socket()) {
+            socket.connect(new InetSocketAddress(ipAddress, DEFAULT_API_PORT), DISCOVERY_DEFAULT_IP_TIMEOUT_RATE_MS);
+
+            OutputStream output = socket.getOutputStream();
+            PrintWriter writer = new PrintWriter(output, true);
+
+            // query the component to see if it has video zones, the device type, friendly name, and serial number
+            writer.println("01/1/GET_NUM_ZONES:");
+            writer.println("01/1/GET_DEVICE_TYPE_NAME:");
+            writer.println("01/1/GET_FRIENDLY_NAME:");
+            writer.println("01/1/GET_DEVICE_INFO:");
+
+            InputStream input = socket.getInputStream();
+
+            BufferedReader reader = new BufferedReader(new InputStreamReader(input));
+
+            ThingTypeUID thingTypeUid = THING_TYPE_PLAYER;
+            String friendlyName = EMPTY;
+            String serialNumber = EMPTY;
+            String componentType = EMPTY;
+            String line;
+            String videoZone = null;
+            String audioZone = null;
+            int lineCount = 0;
 
-        InetAddress localHost = InetAddress.getLocalHost();
-        NetworkInterface networkInterface = NetworkInterface.getByInetAddress(localHost);
+            while ((line = reader.readLine()) != null) {
+                String[] strArr = line.split(":");
 
-        for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) {
-            InetAddress ipAddress = address.getAddress();
+                if (strArr.length >= 4) {
+                    switch (strArr[1]) {
+                        case "NUM_ZONES":
+                            videoZone = strArr[2];
+                            audioZone = strArr[3];
+                            break;
+                        case "DEVICE_TYPE_NAME":
+                            componentType = strArr[2];
+                            break;
+                        case "FRIENDLY_NAME":
+                            friendlyName = strArr[2];
+                            break;
+                        case "DEVICE_INFO":
+                            serialNumber = strArr[3].trim(); // take off leading zeros
+                            break;
+                    }
+                } else {
+                    logger.debug("isKaleidescapeDevice() - Unable to process line: {}", line);
+                }
 
-            String cidrSubnet = ipAddress.getHostAddress() + "/" + address.getNetworkPrefixLength();
+                lineCount++;
 
-            /* Apache Subnet Utils only supports IP v4 for creating string list of IP's */
-            if (ipAddress instanceof Inet4Address) {
-                logger.debug("Found interface IPv4 address to scan: {}", cidrSubnet);
+                // stop after reading four lines
+                if (lineCount > 3) {
+                    break;
+                }
+            }
+
+            // see if we have a video zone
+            if ("01".equals(videoZone)) {
+                // now check if we are one of the allowed types
+                if (ALLOWED_DEVICES.contains(componentType)) {
+                    if (STRATO_S.equals(componentType) || STRATO.equals(componentType)) {
+                        thingTypeUid = THING_TYPE_STRATO;
+                    }
+
+                    // A 'Player' without an audio zone is really a Strato C
+                    // does not work yet, Strato C erroneously reports "01" for audio zones
+                    // so we are unable to differentiate a Strato C from a Premiere player
+                    if ("00".equals(audioZone) && PLAYER.equals(componentType)) {
+                        thingTypeUid = THING_TYPE_STRATO;
+                    }
+
+                    // Alto
+                    if (ALTO.equals(componentType)) {
+                        thingTypeUid = THING_TYPE_ALTO;
+                    }
 
-                SubnetUtils utils = new SubnetUtils(cidrSubnet);
+                    // Cinema One
+                    if (CINEMA_ONE.equals(componentType)) {
+                        thingTypeUid = THING_TYPE_CINEMA_ONE;
+                    }
 
-                results.addAll(Arrays.asList(utils.getInfo().getAllAddresses())); // not sure how to do this without the
-                                                                                  // Apache libraries
-            } else if (ipAddress instanceof Inet6Address) {
-                logger.debug("Found interface IPv6 address to scan: {}, ignoring", cidrSubnet);
+                    // A Disc Vault with a video zone (the M700 vault), just call it a THING_TYPE_PLAYER
+                    if (DISC_VAULT.equals(componentType)) {
+                        thingTypeUid = THING_TYPE_PLAYER;
+                    }
+
+                    // default THING_TYPE_PLAYER
+                    submitDiscoveryResults(thingTypeUid, ipAddress, friendlyName, serialNumber);
+                }
             } else {
-                logger.debug("Found interface unknown IP type address to scan: {}", cidrSubnet);
+                logger.debug("No Suitable Kaleidescape component found at IP address ({})", ipAddress);
             }
+            reader.close();
+            input.close();
+            writer.close();
+            output.close();
+            socket.close();
+        } catch (IOException e) {
+            logger.debug("isKaleidescapeDevice() IOException: {}", e.getMessage());
         }
+    }
 
-        return results;
+    /**
+     * Create a new Thing with an IP address and Component type given. Uses default port.
+     *
+     * @param thingTypeUid ThingTypeUID of detected Kaleidescape component.
+     * @param ip IP address of the Kaleidescape component as a string.
+     * @param friendlyName Name of Kaleidescape component as a string.
+     * @param serialNumber Serial Number of Kaleidescape component as a string.
+     */
+    private void submitDiscoveryResults(ThingTypeUID thingTypeUid, String ip, String friendlyName,
+            String serialNumber) {
+        ThingUID uid = new ThingUID(thingTypeUid, serialNumber);
+
+        HashMap<String, Object> properties = new HashMap<>();
+
+        properties.put("host", ip);
+        properties.put("port", DEFAULT_API_PORT);
+
+        thingDiscovered(DiscoveryResultBuilder.create(uid).withProperties(properties).withRepresentationProperty("host")
+                .withLabel(friendlyName).build());
     }
 }