/**
* 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.primare.internal.protocol;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import org.openhab.binding.primare.internal.PrimareStatusUpdateEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Connector for Primare TCP communication. This connector has only
* been tested with a linux box running socat between a socket and a
* serial interface, not a Primare device with a built-in Ethernet
* interface.
*
* @author juslive
* @since 1.7.0
*/
public class PrimareTCPConnector extends PrimareConnector {
private static final Logger logger = LoggerFactory.getLogger(PrimareTCPConnector.class);
/** Connection timeout in milliseconds **/
private static final int CONNECTION_TIMEOUT = 5000;
/** Connection test interval in milliseconds **/
private static final int CONNECTION_TEST_INTERVAL = 15000;
/** Socket timeout in milliseconds **/
private static final int SOCKET_TIMEOUT = CONNECTION_TEST_INTERVAL + 10000;
// Locks
private final Object oLock = new Object();
private final Object iLock = new Object();
private String host;
private int port;
private Socket socket = null;
private DataListener dataListener = null;
private DataInputStream inStream = null;
private OutputStream outStream;
private ConnectionSupervisor connectionSupervisor = null;
public PrimareTCPConnector(String deviceId, String host, int port, PrimareMessageFactory mf,
PrimareResponseFactory rf) {
this.deviceId = deviceId;
this.host = host;
this.port = port;
this.messageFactory = mf;
this.responseFactory = rf;
}
public static <T extends PrimareTCPConnector> T newForModel(String m, String deviceId, String host, int port) {
T pc = null;
if (m == null) {
logger.error("connectorForModel called with null argument");
return null;
}
if ("SP31.7".equals(m) || "SP31".equals(m) || "SPA20".equals(m) || "SPA21".equals(m)) {
pc = (T) new org.openhab.binding.primare.internal.protocol.spa20.PrimareSPA20TCPConnector(deviceId, host,
port);
} else {
logger.error("Could not find PrimareTCPConnector for Primare model {m}");
}
return pc;
}
@Override
public String toString() {
return String.format("[%s at %s:%s (%s%s)]", deviceId, host, port,
socket != null && socket.isConnected() ? "connected" : "not connected",
connectionBroken ? ",broken" : "");
}
@Override
public boolean isConnected() {
return (socket != null && socket.isConnected());
}
private boolean needRestart() {
return (connectionBroken || (socket != null && !socket.isConnected()));
}
@Override
public void connect() {
logger.debug("Connecting to {}", this.toString());
try {
connectSocket();
} catch (Exception unimportant) {
}
// Start connection supervisor regardless of initial connection status
if (connectionSupervisor == null) {
logger.trace("Starting connection supervisor for {}", this.toString());
connectionSupervisor = new ConnectionSupervisor(CONNECTION_TEST_INTERVAL);
logger.trace("Started connection supervisor for {}", this.toString());
}
}
@Override
public void disconnect() {
disconnectSocket();
}
@Override
public void sendBytes(byte[] msg) throws IOException {
logger.trace("Sending (hex) [{}] to {} via TCP", PrimareUtils.byteArrayToHex(msg), this.toString());
try {
synchronized (oLock) {
outStream.write(msg);
outStream.flush();
}
bytesSentAt = new Date();
logger.trace("Sent and flushed (hex) [{}] to {} via TCP", PrimareUtils.byteArrayToHex(msg),
this.toString());
} catch (SocketException e) {
connectionBroken = true;
throw e;
} catch (IOException e) {
connectionBroken = true;
throw e;
}
}
private void connectSocket() throws UnknownHostException, IOException {
//
// Do not connect if we have a valid connection
//
if (socket != null && (connectionBroken || !socket.isConnected())) {
disconnect(); // cleanup
}
if (socket == null) {
try {
// Creating a socket to connect to the server
socket = new Socket();
socket.connect(new InetSocketAddress(host, port), CONNECTION_TIMEOUT);
logger.debug("Socket connected to {}", PrimareTCPConnector.this.toString());
inStream = new DataInputStream(socket.getInputStream());
outStream = socket.getOutputStream();
socket.setSoTimeout(SOCKET_TIMEOUT);
outStream.flush();
// Start DataListener before sending init message in case there is a response
logger.trace("connect - starting DataListener for {}", PrimareTCPConnector.this.toString());
dataListener = new DataListener();
dataListener.start();
logger.trace("connect - started DataListener update listener for {}", this.toString());
final PrimareMessage[] deviceInitMessages = messageFactory.getInitMessages();
if (deviceInitMessages != null) {
try {
logger.trace("Sending init messages to {}", this.toString());
/*
* logger.trace("Sending init messages (hex) [{}] to {}",
* PrimareUtils.byteArrayToHex(deviceInitMessage.escaped()),
* this.toString());
*/
sendMessage(deviceInitMessages);
} catch (Exception e) {
/*
* logger.warn("Failed to send init message {} to {} at {}:{}",
* PrimareUtils.byteArrayToHex(deviceInitMessage.escaped()),this.toString());
*/
logger.warn("Failed to send init messages to {}", this.toString());
return;
}
} else {
logger.trace("No init message found for {}", this.toString());
}
} catch (UnknownHostException unknownHost) {
logger.error("connect - unknown host for {} at", this.toString());
throw unknownHost;
} catch (IOException ioException) {
logger.error("Can not connect to socket for {} : {}", this.toString(), ioException.getMessage());
throw ioException;
}
}
}
private void disconnectSocket() {
if (socket != null) {
try {
socket.close();
} catch (Exception unimportant) {
}
socket = null;
connectionBroken = false;
}
}
private class DataListener extends Thread {
private boolean interrupted = false;
DataListener() {
}
public void setInterrupted(boolean interrupted) {
this.interrupted = interrupted;
this.interrupt();
}
@Override
public void run() {
logger.debug("DataListener for {} started", PrimareTCPConnector.this.toString());
// as long as we are connected and no interrupt is requested, continue running
while (!connectionBroken && socket != null && socket.isConnected() && !interrupted) {
try {
waitStateMessages();
} catch (SocketTimeoutException e) {
logger.debug("No data received from {} during supervision interval ({} sec)!",
PrimareTCPConnector.this.toString(), SOCKET_TIMEOUT);
} catch (Exception e) {
if (interrupted != true && this.isInterrupted() != true) {
logger.error("Error for {} during message waiting: {}", PrimareTCPConnector.this.toString(), e);
// Unspecified problem, let's mark this connection broken
connectionBroken = true;
break;
}
}
}
logger.debug("DataListener for {} stopped", PrimareTCPConnector.this.toString());
}
/**
* Read bytes from inStream
*
* @throws IOException
* @throws InterruptedException
**/
private void waitStateMessages() throws NumberFormatException, IOException, InterruptedException {
PrimareStatusUpdateEvent event = new PrimareStatusUpdateEvent(this);
logger.debug("Entered waitStateMessages loop for {}", PrimareTCPConnector.this.toString());
while (true) {
logger.trace("waitStateMessages - waiting for data");
byte b = inStream.readByte();
bytesReceivedAt = new Date();
// logger.trace("waitStateMessages - data from {} READ: [{}] ({})",
// PrimareTCPConnector.this.toString(), String.format("0x%02x",b), b);
buffer[total++] = b;
// Access byte[] buffer and consume bytes 0 .. total-1 if DLE ETX has been seen
parseData(total - 1);
}
}
}
private class ConnectionSupervisor {
private Timer timer;
public ConnectionSupervisor(int milliseconds) {
logger.debug("Connection supervisor started, interval {} milliseconds", milliseconds);
timer = new Timer();
timer.schedule(new ConnectionSupervisorTask(), milliseconds, milliseconds);
}
public void activate(int milliseconds) {
logger.debug("Connection supervisor (re)activated, interval {} milliseconds", milliseconds);
timer.schedule(new ConnectionSupervisorTask(), milliseconds, milliseconds);
}
public void deactivate() {
logger.debug("Connection supervisor deactivated");
timer.cancel();
}
class ConnectionSupervisorTask extends TimerTask {
@Override
public void run() {
logger.debug("Scheduled connection supervisor task started for {}",
PrimareTCPConnector.this.toString());
if (needRestart()) {
disconnectSocket(); // cleanup
}
if (socket == null) {
logger.debug("No connection, connecting to {}", PrimareTCPConnector.this.toString());
try {
connectSocket();
} catch (Exception unimportant) {
logger.debug("Still no connection after retry, failed to connect to {}",
PrimareTCPConnector.this.toString());
}
} else {
logger.debug("Connection to {} exists, last message sent:{} received:{}",
PrimareTCPConnector.this.toString(), messageSentAt, messageReceivedAt);
try {
sendPingMessages();
} catch (Exception unimportant) {
logger.trace("Connection to {} send ping message failed", PrimareTCPConnector.this.toString());
// Variable connectionBroken has already been set by sendBytes,
// no need to set it here
}
}
}
}
}
}