/**
* Copyright (c) 2014-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.io.harmonyhub;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HarmonyHubDiscovery} class discovers Logitech Harmony Hubs on the
* network by broadcasting a discovery string to port 5224 on the
* broadcast address. Hubs respond by making a TCP connection back to
* the IP address that our packet was sent from and on the port we
* advertised as part of the original discovery request.
*
* @author Dan Cunningham - Initial contribution
*
*/
public class HarmonyHubDiscovery {
private Logger logger = LoggerFactory.getLogger(HarmonyHubDiscovery.class);
// notice the port appended to the end of the string
private static final String DISCO_STRING = "_logitech-reverse-bonjour._tcp.local.\n%d";
private static final int DISCO_PORT = 5224;
static protected final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private ScheduledFuture<?> broadcastFuture;
private ScheduledFuture<?> timeoutFuture;
private ServerSocket serverSocket;
private HarmonyServer server;
private int timeout;
private boolean running;
private String optionalHost;
private List<HarmonyHubDiscoveryListener> listeners = new CopyOnWriteArrayList<HarmonyHubDiscoveryListener>();
/**
*
* @param timeout
* how long we discover for
*/
public HarmonyHubDiscovery(int timeout, String optionalHost) {
this.timeout = timeout;
this.optionalHost = optionalHost;
running = false;
listeners = new LinkedList<HarmonyHubDiscoveryListener>();
}
/**
* Adds a HarmonyHubDiscoveryListener
*
* @param listener
*/
public void addListener(HarmonyHubDiscoveryListener listener) {
listeners.add(listener);
}
/**
* Removes a HarmonyHubDiscoveryListener
*
* @param listener
*/
public void removeListener(HarmonyHubDiscoveryListener listener) {
listeners.remove(listener);
}
/**
* Starts discovery for Harmony Hubs
*/
public synchronized void startDiscovery() {
if (running) {
return;
}
try {
serverSocket = new ServerSocket(0);
logger.debug("Creating Harmony server on port {}", serverSocket.getLocalPort());
server = new HarmonyServer(serverSocket);
server.start();
broadcastFuture = scheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
sendDiscoveryMessage(String.format(DISCO_STRING, serverSocket.getLocalPort()));
}
}, 0, 2, TimeUnit.SECONDS);
timeoutFuture = scheduler.schedule(new Runnable() {
@Override
public void run() {
stopDiscovery();
}
}, timeout, TimeUnit.SECONDS);
running = true;
} catch (IOException e) {
logger.error("Could not start Harmony discovery server ", e);
}
}
/**
* Stops discovery of Harmony Hubs
*/
public synchronized void stopDiscovery() {
if (broadcastFuture != null) {
broadcastFuture.cancel(true);
}
if (timeoutFuture != null) {
broadcastFuture.cancel(true);
}
if (server != null) {
server.setRunning(false);
}
try {
serverSocket.close();
} catch (Exception e) {
logger.error("Could not stop harmony discovery socket", e);
}
for (HarmonyHubDiscoveryListener listener : listeners) {
listener.hubDiscoveryFinished();
}
running = false;
}
/**
* Send broadcast message over all active interfaces
*
* @param discoverString
* String to be used for the discovery
*/
private void sendDiscoveryMessage(String discoverString) {
DatagramSocket bcSend = null;
try {
bcSend = new DatagramSocket();
bcSend.setBroadcast(true);
byte[] sendData = discoverString.getBytes();
// Broadcast the message over all the network interfaces
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
if (networkInterface.isLoopback() || !networkInterface.isUp()) {
continue;
}
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
InetAddress[] broadcast = new InetAddress[3];
broadcast[0] = InetAddress.getByName("224.0.0.1");
broadcast[1] = InetAddress.getByName("255.255.255.255");
broadcast[2] = interfaceAddress.getBroadcast();
broadcast[3] = InetAddress.getByName(optionalHost);
for (InetAddress bc : broadcast) {
// Send the broadcast package!
if (bc != null && !bc.isLoopbackAddress()) {
try {
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, bc,
DISCO_PORT);
bcSend.send(sendPacket);
} catch (IOException e) {
logger.error("IO error during HarmonyHub discovery: {}", e.getMessage());
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
logger.trace("Request packet sent to: {} Interface: {}", bc.getHostAddress(),
networkInterface.getDisplayName());
}
}
}
}
} catch (IOException e) {
logger.debug("IO error during HarmonyHub discovery: {}", e.getMessage());
} finally {
try {
if (bcSend != null) {
bcSend.close();
}
} catch (Exception e) {
// Ignore
}
}
}
/**
* Server which accepts TCP connections from Harmony Hubs during the discovery process
*
* @author Dan Cunningham - Initial contribution
*
*/
private class HarmonyServer extends Thread {
private ServerSocket serverSocket;
private boolean running;
private List<String> responses = new ArrayList<String>();
public HarmonyServer(ServerSocket serverSocket) {
this.serverSocket = serverSocket;
running = true;
}
@Override
public void run() {
while (running) {
Socket socket = null;
try {
socket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String input;
while ((input = in.readLine()) != null) {
if (!running) {
break;
}
logger.trace("READ {}", input);
String propsString = input.replaceAll(";", "\n");
propsString = propsString.replaceAll(":", "=");
Properties props = new Properties();
props.load(new StringReader(propsString));
if (!responses.contains(props.getProperty("friendlyName"))) {
responses.add(props.getProperty("friendlyName"));
HarmonyHubDiscoveryResult result = new HarmonyHubDiscoveryResult(props.getProperty("ip"),
props.getProperty("accountId"), props.getProperty("uuid"),
props.getProperty("host_name").replaceAll("[^A-Za-z0-9\\-_]", ""),
props.getProperty("friendlyName"));
for (HarmonyHubDiscoveryListener listener : listeners) {
listener.hubDiscovered(result);
}
}
}
} catch (IOException e) {
if (running) {
logger.debug("Error connecting with found hub", e);
}
} finally {
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
logger.warn("could not close socket", e);
}
}
}
}
public void setRunning(boolean running) {
this.running = running;
}
}
}