From 1fb6fad0790a768e2c132a1aa599d30ca33b7859 Mon Sep 17 00:00:00 2001 From: mlobstein Date: Sat, 18 Sep 2021 06:51:30 -0500 Subject: [PATCH] Improve discovery service (#11258) Signed-off-by: Michael Lobstein Signed-off-by: Dave J Schoepel --- .../README.md | 5 +- .../discovery/KaleidescapeDiscoveryJob.java | 209 ------------- .../KaleidescapeDiscoveryService.java | 276 ++++++++++++++---- 3 files changed, 219 insertions(+), 271 deletions(-) delete mode 100644 bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryJob.java diff --git a/bundles/org.openhab.binding.kaleidescape/README.md b/bundles/org.openhab.binding.kaleidescape/README.md index b92a7f9b10ac..29b00a059b7f 100644 --- a/bundles/org.openhab.binding.kaleidescape/README.md +++ b/bundles/org.openhab.binding.kaleidescape/README.md @@ -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 index f9f5b6130e90..000000000000 --- a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryJob.java +++ /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 ALLOWED_DEVICES = new HashSet( - 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; - } -} diff --git a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryService.java b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryService.java index 2505e7fd4fef..8bb2388bbc85 100644 --- a/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryService.java +++ b/bundles/org.openhab.binding.kaleidescape/src/main/java/org/openhab/binding/kaleidescape/internal/discovery/KaleidescapeDiscoveryService.java @@ -14,26 +14,30 @@ 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 @@ * * @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 ALLOWED_DEVICES = new HashSet( + 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 foundIPs = new HashSet(); + @Activate public KaleidescapeDiscoveryService() { super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_DEFAULT_TIMEOUT_RATE_MS, DISCOVERY_DEFAULT_AUTO_DISCOVER); @@ -70,83 +97,214 @@ public Set getSupportedThingTypes() { return SUPPORTED_THING_TYPES_UIDS; } + /** + * {@inheritDoc} + * + * Starts the scan. This discovery will: + *
    + *
  • 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
  • + *
  • 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
  • + *
+ * The process will continue until {@link #stopScan()} is called. + */ @Override protected void startScan() { logger.debug("Starting discovery of Kaleidescape components."); - try { - List 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 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 getIpAddressScanList() throws UnknownHostException, SocketException { - List 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 properties = new HashMap<>(); + + properties.put("host", ip); + properties.put("port", DEFAULT_API_PORT); + + thingDiscovered(DiscoveryResultBuilder.create(uid).withProperties(properties).withRepresentationProperty("host") + .withLabel(friendlyName).build()); } }