+++ /dev/null
-/**
- * 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;
- }
-}
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;
*
* @author Chris Graham - Initial contribution
* @author Michael Lobstein - Adapted for the Kaleidescape binding
- *
+ *
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, configurationPid = "discovery.kaleidescape")
.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);
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());
}
}