Improve discovery service (#11258)

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
mlobstein 2021-09-18 06:51:30 -05:00 committed by GitHub
parent 3fc7c6559c
commit 84ba4fc333
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 224 additions and 276 deletions

View 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

View File

@ -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;
}
}

View 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,27 +97,194 @@ 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();
ExecutorService discoverySearchPool = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE,
new NamedThreadFactory("OH-binding-discovery.kaleidescape", true));
for (String ip : ipList) {
discoverySearchPool.execute(new KaleidescapeDiscoveryJob(this, ip));
}
discoverySearchPool.shutdown();
} catch (Exception exp) {
logger.debug("Kaleidescape discovery service encountered an error while scanning for components: {}",
exp.getMessage());
if (executorService != null) {
stopScan();
}
logger.debug("Completed discovery of Kaleidescape components.");
final ExecutorService service = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE,
new NamedThreadFactory("OH-binding-discovery.kaleidescape", true));
executorService = service;
scanning = true;
foundIPs.clear();
service.execute(() -> {
try {
DatagramSocket dSocket = new DatagramSocket(K_HEARTBEAT_PORT);
dSocket.setSoTimeout(DISCOVERY_DEFAULT_TIMEOUT_RATE_MS);
dSocket.setBroadcast(true);
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);
}
});
}
/**
* {@inheritDoc}
*
* 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}
*/
@Override
protected synchronized void stopScan() {
super.stopScan();
ExecutorService service = executorService;
if (service == null) {
return;
}
scanning = false;
try {
service.awaitTermination(DISCOVERY_DEFAULT_TIMEOUT_RATE_MS * 5, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}
service.shutdown();
executorService = null;
}
/**
* 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.
*
* @param ipAddress IP address to connect to
*/
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;
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;
}
// 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;
}
// Cinema One
if (CINEMA_ONE.equals(componentType)) {
thingTypeUid = THING_TYPE_CINEMA_ONE;
}
// 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("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());
}
}
/**
@ -101,7 +295,8 @@ public class KaleidescapeDiscoveryService extends AbstractDiscoveryService {
* @param friendlyName Name of Kaleidescape component as a string.
* @param serialNumber Serial Number of Kaleidescape component as a string.
*/
public void submitDiscoveryResults(ThingTypeUID thingTypeUid, String ip, String friendlyName, String serialNumber) {
private void submitDiscoveryResults(ThingTypeUID thingTypeUid, String ip, String friendlyName,
String serialNumber) {
ThingUID uid = new ThingUID(thingTypeUid, serialNumber);
HashMap<String, Object> properties = new HashMap<>();
@ -112,41 +307,4 @@ public class KaleidescapeDiscoveryService extends AbstractDiscoveryService {
thingDiscovered(DiscoveryResultBuilder.create(uid).withProperties(properties).withRepresentationProperty("host")
.withLabel(friendlyName).build());
}
/**
* Provide a string list of all the IP addresses associated with the network interfaces on
* this machine.
*
* @return String list of IP addresses.
* @throws UnknownHostException
* @throws SocketException
*/
private List<String> getIpAddressScanList() throws UnknownHostException, SocketException {
List<String> results = new ArrayList<>();
InetAddress localHost = InetAddress.getLocalHost();
NetworkInterface networkInterface = NetworkInterface.getByInetAddress(localHost);
for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) {
InetAddress ipAddress = address.getAddress();
String cidrSubnet = ipAddress.getHostAddress() + "/" + address.getNetworkPrefixLength();
/* 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);
SubnetUtils utils = new SubnetUtils(cidrSubnet);
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);
} else {
logger.debug("Found interface unknown IP type address to scan: {}", cidrSubnet);
}
}
return results;
}
}