/**
* Copyright (c) 2010-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.binding.knx.internal.connection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.commons.lang.StringUtils;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import gnu.io.CommPortIdentifier;
import gnu.io.RXTXVersion;
import tuwien.auto.calimero.CloseEvent;
import tuwien.auto.calimero.FrameEvent;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.exception.KNXException;
import tuwien.auto.calimero.knxnetip.KNXnetIPConnection;
import tuwien.auto.calimero.link.KNXNetworkLink;
import tuwien.auto.calimero.link.KNXNetworkLinkFT12;
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
import tuwien.auto.calimero.link.NetworkLinkListener;
import tuwien.auto.calimero.link.medium.TPSettings;
import tuwien.auto.calimero.process.ProcessCommunicator;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
import tuwien.auto.calimero.process.ProcessListener;
/**
* This class establishes the connection to the KNX bus.
* It uses the ConfigAdmin service to retrieve the relevant configuration data.
*
* @author Kai Kreuzer
*
*/
public class KNXConnection implements ManagedService {
private static final Logger sLogger = LoggerFactory.getLogger(KNXConnection.class);
private static ProcessCommunicator sPC = null;
private static ProcessListener sProcessCommunicationListener = null;
private static KNXNetworkLink sLink;
/** signals that the connection is shut down on purpose */
public static boolean sShutdown = false;
/** the ip address to use for connecting to the KNX bus */
private static String sIp;
/** the KNX bus address to use as local sourceaddress in KNX bus */
private static String sLocalSourceAddr = "0.0.0";
/** the KNX bus address to use as local sourceaddress in KNX bus */
private static boolean sIgnoreLocalSourceEvents = false;
/** the ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTING */
private static int sIpConnectionType;
/**
* the default multicast ip address (see
* <a href="http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml">iana</a> EIBnet/IP)
*/
private static final String DEFAULT_MULTICAST_IP = "224.0.23.12";
/** KNXnet/IP port number */
private static int sPort;
/** local endpoint to specify the multicast interface. no port is used */
private static String sLocalIp;
/** the serial port to use for connecting to the KNX bus */
private static String sSerialPort;
/**
* time in milliseconds of how long should be paused between two read requests to the bus during initialization.
* Default value is <code>50</Code>
*/
private static long sReadingPause = 50;
/** timeout in milliseconds to wait for a response from the KNX bus. Default value is <code>10000</code> */
private static long sResponseTimeout = 10000;
/** limits the read retries while initialization from the KNX bus. Default value is <code>3</code> */
private static int sReadRetriesLimit = 3;
/**
* seconds between connect retries when KNX link has been lost, 0 means never retry. Default value is <code>0</code>
*/
private static int sAutoReconnectPeriod = 0;
/** Number of threads executing in parallel for auto refresh feature. Default value is <code>5</code> */
private static int sNumberOfThreads = 5;
/**
* Time in seconds to wait for an orderly shutdown of the auto refresher feature. Default value is <code>5</code>
*/
private static int sScheduledExecutorServiceShutdownTimeout = 5;
/**
* The maximum number of queue entries in the refresh queue used by the auto refresh feature. Default value is
* <code>10000</code>
*/
private static int sMaxRefreshQueueEntries = 10000;
/**
* Determines whether Network Address Translation (NAT) will be used for IP connections.
*
* Default value is <code>false</code>.
*/
private static boolean sUseNAT = false;
/** listeners for connection/re-connection events */
private static Set<KNXConnectionListener> sConnectionListeners = new HashSet<KNXConnectionListener>();
/**
* Returns the KNXNetworkLink for talking to the KNX bus.
* The link can be null, if it has not (yet) been established successfully.
*
* @return the KNX network link
*/
public static synchronized ProcessCommunicator getCommunicator() {
if (sLink != null && !sLink.isOpen()) {
connect();
}
return sPC;
}
public void setProcessListener(ProcessListener listener) {
if (sPC != null) {
sPC.removeProcessListener(KNXConnection.sProcessCommunicationListener);
sLogger.debug("Adding Process Listener: {}", listener);
sPC.addProcessListener(listener);
}
KNXConnection.sProcessCommunicationListener = listener;
}
public void unsetProcessListener(ProcessListener listener) {
if (sPC != null) {
sPC.removeProcessListener(KNXConnection.sProcessCommunicationListener);
}
KNXConnection.sProcessCommunicationListener = null;
}
public static void addConnectionListener(KNXConnectionListener listener) {
KNXConnection.sConnectionListeners.add(listener);
}
public static void removeConnectionListener(KNXConnectionListener listener) {
KNXConnection.sConnectionListeners.remove(listener);
}
/**
* Tries to connect either by IP or serial bus, depending on supplied config data.
*
* @return true if connection was established, false otherwise
*/
public static synchronized boolean connect() {
boolean successRetVal = false;
sShutdown = false;
try {
if (StringUtils.isNotBlank(sIp)) {
sLink = connectByIp(sIpConnectionType, sLocalIp, sIp, sPort);
} else if (StringUtils.isNotBlank(sSerialPort)) {
sLink = connectBySerial(sSerialPort);
} else {
sLogger.error("No IP address or serial port could be found in configuration!");
return false;
}
NetworkLinkListener linkListener = new NetworkLinkListener() {
@Override
public void linkClosed(CloseEvent e) {
if (!(CloseEvent.USER_REQUEST == e.getInitiator()) && !sShutdown) {
sLogger.warn("KNX link has been lost (reason: {} on object {})", e.getReason(),
e.getSource().toString());
for (KNXConnectionListener listener : KNXConnection.sConnectionListeners) {
listener.connectionLost();
}
/*
* If an auto reconnect period was scheduled, then start a timer based task, which will
* try to reconnect.
*/
if (sAutoReconnectPeriod > 0) {
sLogger.info("KNX link will be retried in " + sAutoReconnectPeriod + " seconds");
final Timer timer = new Timer();
TimerTask timerTask = new ConnectTimerTask(timer);
timer.schedule(timerTask, sAutoReconnectPeriod * 1000, sAutoReconnectPeriod * 1000);
}
}
}
@Override
public void indication(FrameEvent e) {
}
@Override
public void confirmation(FrameEvent e) {
}
};
sLink.addLinkListener(linkListener);
if (sPC != null) {
sPC.removeProcessListener(sProcessCommunicationListener);
sPC.detach();
}
sPC = new ProcessCommunicatorImpl(sLink);
sPC.setResponseTimeout((int) sResponseTimeout / 1000);
if (sProcessCommunicationListener != null) {
sPC.addProcessListener(sProcessCommunicationListener);
}
if (sLogger.isInfoEnabled()) {
if (sLink instanceof KNXNetworkLinkIP) {
String ipConnectionTypeString = KNXConnection.sIpConnectionType == KNXNetworkLinkIP.ROUTING
? "ROUTER" : "TUNNEL";
sLogger.info("Established connection to KNX bus on {} in mode {}.", sIp + ":" + sPort,
ipConnectionTypeString);
} else {
sLogger.info("Established connection to KNX bus through FT1.2 on serial port {}.", sSerialPort);
}
}
for (KNXConnectionListener listener : KNXConnection.sConnectionListeners) {
listener.connectionEstablished();
}
successRetVal = true;
} catch (KNXException e) {
sLogger.error("Error connecting to KNX bus: {}", e.getMessage());
} catch (UnknownHostException e) {
sLogger.error("Error connecting to KNX bus (unknown host): {}", e.getMessage());
} catch (InterruptedException e) {
sLogger.error("Error connecting to KNX bus (interrupted): {}", e.getMessage());
}
return successRetVal;
}
public static synchronized void disconnect() {
sShutdown = true;
if (sPC != null) {
KNXNetworkLink link = sPC.detach();
if (sProcessCommunicationListener != null) {
sPC.removeProcessListener(sProcessCommunicationListener);
sProcessCommunicationListener = null;
}
if (link != null) {
sLogger.info("Closing KNX connection");
link.close();
}
}
}
private static KNXNetworkLink connectByIp(int ipConnectionType, String localIp, String ip, int port)
throws KNXException, UnknownHostException, InterruptedException {
InetSocketAddress localEndPoint = null;
if (StringUtils.isNotBlank(localIp)) {
localEndPoint = new InetSocketAddress(localIp, 0);
} else {
try {
InetAddress localHost = InetAddress.getLocalHost();
localEndPoint = new InetSocketAddress(localHost, 0);
} catch (UnknownHostException uhe) {
sLogger.warn(
"Couldn't find an IP address for this host. Please check the .hosts configuration or use the 'localIp' parameter to configure a valid IP address.");
}
}
return new KNXNetworkLinkIP(ipConnectionType, localEndPoint, new InetSocketAddress(ip, port), sUseNAT,
new TPSettings(new IndividualAddress(sLocalSourceAddr), true));
}
private static KNXNetworkLink connectBySerial(String serialPort) throws KNXException {
try {
RXTXVersion.getVersion();
return new KNXNetworkLinkFT12(serialPort, new TPSettings(true));
} catch (NoClassDefFoundError e) {
throw new KNXException(
"The serial FT1.2 KNX connection requires the RXTX libraries to be available, but they could not be found!");
} catch (KNXException knxe) {
if (knxe.getMessage().startsWith("can not open serial port")) {
StringBuilder sb = new StringBuilder("Available ports are:\n");
Enumeration<?> portList = CommPortIdentifier.getPortIdentifiers();
while (portList.hasMoreElements()) {
CommPortIdentifier id = (CommPortIdentifier) portList.nextElement();
if (id.getPortType() == CommPortIdentifier.PORT_SERIAL) {
sb.append(id.getName() + "\n");
}
}
sb.deleteCharAt(sb.length() - 1);
knxe = new KNXException("Serial port '" + serialPort + "' could not be opened. " + sb.toString());
}
throw knxe;
}
}
/*
* (non-Javadoc)
*
* @see org.osgi.service.cm.ManagedService#updated(java.util.Dictionary)
*/
@Override
public void updated(Dictionary<String, ?> config) throws ConfigurationException {
if (config != null) {
sLogger.debug("KNXBinding configuration present. Setting up KNX bus connection.");
sIp = (String) config.get("ip");
String readingBusAddrString = (String) config.get("busaddr");
if (StringUtils.isNotBlank(readingBusAddrString)) {
sLocalSourceAddr = readingBusAddrString;
}
String readingIgnLocEv = (String) config.get("ignorelocalevents");
if (StringUtils.isNotBlank(readingIgnLocEv)) {
sIgnoreLocalSourceEvents = readingIgnLocEv.equalsIgnoreCase("true");
}
String connectionTypeString = (String) config.get("type");
if (StringUtils.isNotBlank(connectionTypeString)) {
if ("TUNNEL".equals(connectionTypeString)) {
sIpConnectionType = KNXNetworkLinkIP.TUNNELING;
} else if ("ROUTER".equals(connectionTypeString)) {
sIpConnectionType = KNXNetworkLinkIP.ROUTING;
if (StringUtils.isBlank(sIp)) {
sIp = DEFAULT_MULTICAST_IP;
}
} else {
throw new ConfigurationException("type", "unknown IP connection type '" + connectionTypeString
+ "'! Known types are either 'TUNNEL' or 'ROUTER'");
}
} else {
sIpConnectionType = KNXNetworkLinkIP.TUNNELING;
}
String portConfig = (String) config.get("port");
if (StringUtils.isNotBlank(portConfig)) {
sPort = Integer.parseInt(portConfig);
} else {
sPort = KNXnetIPConnection.DEFAULT_PORT;
}
sLocalIp = (String) config.get("localIp");
sSerialPort = (String) config.get("serialPort");
String readingPauseString = (String) config.get("pause");
if (StringUtils.isNotBlank(readingPauseString)) {
sReadingPause = Long.parseLong(readingPauseString);
}
String responseTimeoutString = (String) config.get("timeout");
if (StringUtils.isNotBlank(responseTimeoutString)) {
long timeout = Long.parseLong(responseTimeoutString);
if (timeout > 0) {
sResponseTimeout = timeout;
}
}
String readRetriesLimitString = (String) config.get("readRetries");
if (StringUtils.isNotBlank(readRetriesLimitString)) {
int readRetries = Integer.parseInt(readRetriesLimitString);
if (readRetries > 0) {
sReadRetriesLimit = readRetries;
}
}
String autoReconnectPeriodString = (String) config.get("autoReconnectPeriod");
if (StringUtils.isNotBlank(autoReconnectPeriodString)) {
int autoReconnectPeriodValue = Integer.parseInt(autoReconnectPeriodString);
if (autoReconnectPeriodValue >= 0) {
sAutoReconnectPeriod = autoReconnectPeriodValue;
}
}
String maxRefreshQueueEntriesString = (String) config.get("maxRefreshQueueEntries");
if (StringUtils.isNotBlank(maxRefreshQueueEntriesString)) {
try {
int maxRefreshQueueEntriesValue = Integer.parseInt(maxRefreshQueueEntriesString);
if (maxRefreshQueueEntriesValue >= 0) {
sMaxRefreshQueueEntries = maxRefreshQueueEntriesValue;
}
} catch (NumberFormatException e) {
sLogger.warn(
"Error when trying to read parameter 'maxRefreshQueueEntries' from configuration. '{}' is not a number: using default.",
maxRefreshQueueEntriesString);
}
}
String numberOfThreadsString = (String) config.get("numberOfThreads");
if (StringUtils.isNotBlank(numberOfThreadsString)) {
try {
int numberOfThreadsValue = Integer.parseInt(numberOfThreadsString);
if (numberOfThreadsValue >= 0) {
sNumberOfThreads = numberOfThreadsValue;
}
} catch (NumberFormatException e) {
sLogger.warn(
"Error when trying to read parameter 'numberOfThreads' from configuration. '{}' is not a number: using default.",
numberOfThreadsString);
}
}
String scheduledExecutorServiceShutdownTimeoutString = (String) config
.get("scheduledExecutorServiceShutdownTimeout");
if (StringUtils.isNotBlank(scheduledExecutorServiceShutdownTimeoutString)) {
try {
int scheduledExecutorServiceShutdownTimeoutValue = Integer
.parseInt(scheduledExecutorServiceShutdownTimeoutString);
if (scheduledExecutorServiceShutdownTimeoutValue >= 0) {
sScheduledExecutorServiceShutdownTimeout = scheduledExecutorServiceShutdownTimeoutValue;
}
} catch (NumberFormatException e) {
sLogger.warn(
"Error when trying to read parameter 'scheduledExecutorServiceShutdownTimeout' from configuration. '{}' is not a number: using default.",
scheduledExecutorServiceShutdownTimeoutString);
}
}
String shouldUseNAT = (String) config.get("useNAT");
sUseNAT = StringUtils.isNotBlank(shouldUseNAT) && shouldUseNAT.equalsIgnoreCase("true");
if (sPC == null) {
sLogger.debug("Not connected yet. Trying to connect.");
if (!connect()) {
sLogger.warn("Initial connection to KNX bus failed!");
if (sAutoReconnectPeriod > 0) {
sLogger.info("KNX link will be retried in {} seconds", sAutoReconnectPeriod);
final Timer timer = new Timer();
TimerTask timerTask = new ConnectTimerTask(timer);
timer.schedule(timerTask, sAutoReconnectPeriod * 1000, sAutoReconnectPeriod * 1000);
}
} else {
sLogger.debug("Success: connected.");
}
}
} else {
sLogger.info(
"KNXBinding configuration is not present. Please check your configuration file or if not needed remove the KNX addon.");
}
}
public static String getLocalSourceAddr() {
return sLocalSourceAddr;
}
public static boolean getIgnoreLocalSourceEvents() {
return sIgnoreLocalSourceEvents;
}
public static long getReadingPause() {
return sReadingPause;
}
public static int getReadRetriesLimit() {
return sReadRetriesLimit;
}
public static int getAutoReconnectPeriod() {
return sAutoReconnectPeriod;
}
/**
* @return the sNumberOfThreads
*/
public static int getNumberOfThreads() {
return sNumberOfThreads;
}
/**
* @return the sScheduledExecutorServiceShutdownTimeout
*/
public static int getScheduledExecutorServiceShutdownTimeout() {
return sScheduledExecutorServiceShutdownTimeout;
}
/**
* @return the sMaxRefreshQueueEntries
*/
public static int getMaxRefreshQueueEntries() {
return sMaxRefreshQueueEntries;
}
private static final class ConnectTimerTask extends TimerTask {
private final Timer timer;
public ConnectTimerTask(Timer timer) {
this.timer = timer;
}
@Override
public void run() {
if (sShutdown) {
timer.cancel();
} else {
sLogger.info("Trying to (re-)connect to KNX...");
if (connect()) {
sLogger.info("Connected to KNX");
timer.cancel();
} else {
sLogger.info("KNX link will be retried in {} seconds", sAutoReconnectPeriod);
}
}
}
}
}