/**
* 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.ipx800.internal;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.ipx800.internal.Ipx800Config.Ipx800DeviceConfig;
import org.openhab.binding.ipx800.internal.command.Ipx800Port;
import org.openhab.binding.ipx800.internal.command.Ipx800PortType;
import org.openhab.binding.ipx800.internal.itemslot.Ipx800OutputItem;
import org.openhab.core.library.types.OnOffType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Seebag
* @since 1.8.0
*
*/
public class Ipx800DeviceConnector extends Thread {
private static final Logger logger = LoggerFactory.getLogger(Ipx800DeviceConnector.class);
/** End line separator */
private final static String ENDL = "\r\n";
/** Time before reconnecting in case of failure */
private static final int reconnectTimeout = 5000;
/** */
private static final int sendTimeout = 1000;
/** Time before sending a keepalive to device */
private static final int keepaliveTimeout = 30000;
/** Max keep alive failure before reconnecting */
private static final int maxKeepAliveFailure = 1;
/** The configuration */
private Ipx800DeviceConfig config;
/** Interruption indicator for listening thread. */
private boolean interrupted = false;
/** Interruption indicator for listening thread. */
private boolean connected = false;
/** Client socket */
private Socket client;
/** The reader */
private BufferedReader in;
/** The writer */
private PrintWriter out;
/** All the response */
private static enum ResponseType {
NONE,
OK,
GET_INPUT,
GET_INPUTS,
GET_OUTPUTS
};
/** Response expected */
private ResponseType expectedResponse = ResponseType.NONE;
/** */
private String globalResponse = null;
/** List of ipx800 ports */
private Vector<Ipx800Port> portList;
/** Failed keepalive count */
private int failedKeepalive = 0;
/** Waiting for keepalive response */
private boolean waitingKeepaliveResponse = false;
/**
*
* @param config
*/
public Ipx800DeviceConnector(Ipx800DeviceConfig config) {
this.config = config;
createPorts();
logger.debug("Initialisation of Ipx800 device {}", this);
}
@Override
public String toString() {
String descr = this.config.name + "@" + this.config.host;
int i = 0;
for (String extName : this.config.x880extensions) {
i++;
if (extName != null) {
descr += " + " + extName + "@x880." + i;
}
}
i = 0;
for (String extName : this.config.x400extensions) {
i++;
if (extName != null) {
descr += " + " + extName + "@x400." + i;
}
}
return descr;
}
/**
* Create the ports regarding to the configuration
*/
private void createPorts() {
int inputs = Ipx800PortType.INPUT.getPortPerDevice() * (1 + this.config.getX880length());
int analogs = Ipx800PortType.ANALOG.getPortPerDevice() * (1 + this.config.getX400length());
int counters = Ipx800PortType.COUNTER.getPortPerDevice();
portList = new Vector<Ipx800Port>();
for (int i = 0; i < inputs; i++) {
portList.add(new Ipx800Port(Ipx800PortType.INPUT, i + 1, this));
portList.add(new Ipx800Port(Ipx800PortType.OUPUT, i + 1, this));
}
for (int i = 0; i < analogs; i++) {
portList.add(new Ipx800Port(Ipx800PortType.ANALOG, i + 1, this));
}
for (int i = 0; i < counters; i++) {
portList.add(new Ipx800Port(Ipx800PortType.COUNTER, i + 1, this));
}
}
/**
* Retrieve the port with the type and the portNumber
*
* @param type
* @param portNumber
* @return the ipx port or null if not found
*/
public Ipx800Port getPort(Ipx800PortType type, int portNumber) {
for (Ipx800Port port : portList) {
if (port.getCommandType() == type && port.getPortNumber() == portNumber) {
return port;
}
}
return null;
}
/**
* Retrieve the port using a config string
*
* @param configPortString using following format : I01
* @return the ipx port or null if not found
*/
public Ipx800Port getPort(String configPortString, int extensionDelta) {
assert(configPortString.length() == 3);
String prefix = configPortString.substring(0, 1);
int portNumber = Integer.parseInt(configPortString.substring(1));
Ipx800PortType slotType = Ipx800PortType.getSlotByPrefix(prefix);
return getPort(slotType, portNumber + extensionDelta);
}
/**
*
* @param configPortString
* @return
*/
public Ipx800Port getPort(String configPortString) {
return getPort(configPortString, 0);
}
/**
* Return all ports
*
* @return
*/
public Vector<Ipx800Port> getAllPorts() {
return portList;
}
/**
* Get the port number delta (first extension is 8)
*
* @param extensionName
* @return the port number delta or 0 if not found
*/
public int getExtensionDelta(String extensionName) {
for (int i = 0; i < config.getX400length(); i++) {
String extName = config.x400extensions[i];
if (extensionName.equals(extName)) {
return Ipx800PortType.ANALOG.getPortPerDevice() * (i + 1);
}
}
for (int i = 0; i < config.getX880length(); i++) {
String extName = config.x880extensions[i];
if (extensionName.equals(extName)) {
return Ipx800PortType.INPUT.getPortPerDevice() * (i + 1);
}
}
return 0;
}
/**
* Set output of the device sending the command corresponding to the state to the device
*
* @param slot
* @param state
*/
public synchronized void setOutput(Ipx800Port slot, org.openhab.core.types.State state) {
if (slot.getCommandType() == Ipx800PortType.OUPUT) {
if (state != null) {
logger.debug("Sending {} to {}", state, slot);
out.format("Set%02d%d" + ENDL, slot.getPortNumber(), state == OnOffType.ON ? 1 : 0);
}
}
}
/**
* FIXME use only this method using items also for redirect
*
* @param slot
* @param item
*/
public synchronized void setOutput(Ipx800OutputItem item) {
org.openhab.core.types.State state = item.getState();
Ipx800Port port = item.getPort();
if (item.isPulseMode()) {
logger.debug("Sending {} to {} in pulse mode", state, port);
out.format("Set%02d%dp" + ENDL, port.getPortNumber(), state == OnOffType.ON ? 1 : 0);
} else {
logger.debug("Sending {} to {}", state, port);
out.format("Set%02d%d" + ENDL, port.getPortNumber(), state == OnOffType.ON ? 1 : 0);
}
}
/**
* Wait for a response of the ipx800
*
* @return
*/
private synchronized String waitResponse() {
String resp;
try {
logger.debug("Will wait");
wait(sendTimeout);
} catch (InterruptedException e) {
}
resp = globalResponse;
if (globalResponse == null) {
logger.debug("Cannot receive response");
resp = "";
}
globalResponse = null;
return resp;
}
/**
* This should be called from the reception loop to send response to mainthread.
* Disabled for now. Not really useful
*
* @param command
*/
private synchronized void sendResponse(String command) {
globalResponse = command;
notify();
}
/**
*
* @param data
* @param slotType
*/
public void onBitUpdate(String data, Ipx800PortType slotType) {
onBitUpdate(data, slotType, -1);
}
/**
*
* @param data
* @param slotType
* @param slotNumber
*/
public void onBitUpdate(String data, Ipx800PortType slotType, int slotNumber) {
logger.trace("onBitUpdate with data='{}' for type '{}'...", data, slotType.name());
if (slotType == Ipx800PortType.INPUT || slotType == Ipx800PortType.OUPUT) {
if (data.length() != slotType.getMaxSlots()) {
logger.error("Received data doesn't match expected size");
return;
}
}
if (slotNumber >= 0) {
logger.trace("... for slot '{}'", slotNumber);
postUpdate(data, slotType, slotNumber);
} else {
for (int i = 0; i < data.length(); i++) {
postUpdate(data.substring(i), slotType, i + 1);
}
}
}
/**
*
* @param data
* @param slotType
* @param slotNumber
*/
private void postUpdate(String data, Ipx800PortType slotType, int slotNumber) {
Ipx800Port slot = getPort(slotType, slotNumber);
if (slot != null) {
slot.updateStateIfChanged(data);
}
}
/**
*
* @param data
*/
private void unsollicitedUpdate(String data) {
// Example of
// I=00000000000000000000000000000000&O=10000000000000000000000000000000&\
// A0=0&A1=0&A2=0&A3=0&A4=0&A5=0&A6=0&A7=0&A8=0&A9=0&A10=0&A11=0&A12=0&A13=0&A14=0&A15=0&C1=47&C2=0&C3=0&C4=0&C5=0&C6=0&C7=0&C8=0
final Pattern VALIDATION_PATTERN = Pattern.compile("I=(\\d{32})&O=(\\d{32})&([AC]\\d{1,2}=\\d+&)*[^I]*");
final Matcher matcher = VALIDATION_PATTERN.matcher(data);
while (matcher.find()) { // Workaround of an IPX800 bug
String completeCommand = matcher.group();
logger.debug("Command : " + completeCommand);
for (String command : completeCommand.split("&")) {
int sepIndex = command.indexOf("=");
if (sepIndex == -1) {
continue;
}
String prefix = command.substring(0, sepIndex);
Ipx800PortType slotType = Ipx800PortType.getSlotByPrefix(prefix.substring(0, 1));
if (slotType == null) {
logger.error("Not supported type for now '{}'", prefix);
continue;
}
if (sepIndex == 1) {
onBitUpdate(command.substring(sepIndex + 1), slotType);
} else {
onBitUpdate(command.substring(sepIndex + 1), slotType, Integer.parseInt(prefix.substring(1)));
}
}
}
}
/**
* Unused for now
*/
@SuppressWarnings("unused")
private void updateState() {
// Update internal state
expectedResponse = ResponseType.GET_OUTPUTS;
out.print("GetOutputs" + ENDL);
String resp = waitResponse();
if (resp == null) {
// throw exc
return;
}
onBitUpdate(resp, Ipx800PortType.OUPUT);
}
/**
* Connect to the ipx800
*
* @throws IOException
*/
private void connect() throws IOException {
disconnect();
logger.debug("Connecting {}@ {}:{}...", config.name, config.host, config.port);
client = new Socket(config.host, Integer.parseInt(config.port));
client.setSoTimeout(keepaliveTimeout);
client.getInputStream().skip(client.getInputStream().available());
in = new BufferedReader(new InputStreamReader(client.getInputStream()));
out = new PrintWriter(client.getOutputStream(), true);
connected = true;
logger.debug("Connected to {}@ {}:{}", config.name, config.host, config.port);
}
/**
* Disconnect the device
*/
public void disconnect() {
if (connected) {
logger.debug("Disconnecting");
try {
client.close();
} catch (IOException e) {
logger.error("Unable to disconnect {}", e.getMessage());
}
connected = false;
logger.debug("Disconnected");
}
}
/**
* Stop the device thread
*/
public void destroyAndExit() {
interrupted = true;
disconnect();
for (Ipx800Port port : portList) {
port.destroy();
}
}
/**
* Send an arbitrary keepalive command which cause the IPX to send an update.
* If we don't receive the update maxKeepAliveFailure time, the connection is closed and reopened
*/
private void sendKeepalive() {
if (waitingKeepaliveResponse) {
failedKeepalive++;
logger.debug("Sending keepalive, attempt {}", failedKeepalive);
} else {
failedKeepalive = 0;
logger.trace("Sending keepalive");
}
out.println("GetIn01");
out.flush();
waitingKeepaliveResponse = true;
}
@Override
public void run() {
interrupted = false;
while (!interrupted) {
try {
waitingKeepaliveResponse = false;
failedKeepalive = 0;
connect();
String command;
while (!interrupted) {
if (failedKeepalive > maxKeepAliveFailure) {
throw new IOException("Max keep alive attempts has been reached");
}
try {
command = in.readLine();
if (command.equals("0") || command.equals("1")) {
logger.trace("Keepalive response ok");
} else {
logger.debug("Receiving {}", command);
}
waitingKeepaliveResponse = false; // Reseting keepalive state each time we receive a command
unsollicitedUpdate(command);
expectedResponse = ResponseType.NONE;
} catch (SocketTimeoutException e) {
sendKeepalive();
}
}
disconnect();
} catch (IOException e) {
logger.error(e.getMessage() + " will retry in " + reconnectTimeout + "ms");
}
try {
Thread.sleep(reconnectTimeout);
} catch (InterruptedException e) {
logger.error(e.getMessage());
}
}
}
}