/**
* 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.pioneeravr.internal.ipcontrolprotocol;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.pioneeravr.internal.PioneerAvrEventListener;
import org.openhab.binding.pioneeravr.internal.PioneerAvrStatusUpdateEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* A class that wraps the communication to Pioneer devices using the
* Pioneer IpControl protocol
*
* see {@link http://www.pioneerelectronics.com/StaticFiles/PUSA/Files/Home%20Custom%20Install/VSX-1120-K-RS232.PDF} for
* the protocol specs
*
* @author Rainer Ostendorf
* @author based on the Onkyo binding by Pauli Anttila and others
*/
public class IpControl {
private static final Logger logger = LoggerFactory.getLogger(IpControl.class);
/** Instantiated class IP for the receiver to communicate with. **/
private String receiverIP = "";
/** default port for IP communication (23=telnet). **/
public static final int DEFAULT_IPCONTROL_PORT = 23;
/** Connection timeout in milliseconds **/
private static final int CONNECTION_TIMEOUT = 5000;
/** Connection test interval in milliseconds **/
private static final int CONNECTION_TEST_INTERVAL = 60000;
/** Socket timeout in milliseconds **/
private static final int SOCKET_TIMEOUT = CONNECTION_TEST_INTERVAL + 10000;
/** Instantiated class Port for the receiver to communicate with. **/
private int receiverPort = DEFAULT_IPCONTROL_PORT;
private Boolean connectionCheckActive = true;
private static Socket ipControlSocket = null;
private DataListener dataListener = null;
private static DataOutputStream outStream = null;
private static DataInputStream inStream = null;
private static BufferedReader inBufferedReader = null;
private static boolean connected = false;
private static List<PioneerAvrEventListener> _listeners = new ArrayList<PioneerAvrEventListener>();
private static int retryCount = 1;
private static ConnectionSupervisor connectionSupervisor = null;
/**
* Constructor that takes your receivers IP and port.
**/
public IpControl(String ip, int ipControlPort, Boolean doConnectionCheck) {
if (StringUtils.isNotBlank(ip)) {
receiverIP = ip;
}
if (ipControlPort >= 1) {
receiverPort = ipControlPort;
}
connectionCheckActive = doConnectionCheck;
}
/**
* Add event listener, which will be invoked when status upadte is received from receiver.
**/
public synchronized void addEventListener(PioneerAvrEventListener listener) {
_listeners.add(listener);
}
/**
* Remove event listener.
**/
public synchronized void removeEventListener(PioneerAvrEventListener listener) {
_listeners.remove(listener);
}
/**
* Get retry count value.
**/
public static int getRetryCount() {
return retryCount;
}
/**
* Set retry count value. How many times command is retried when error occurs.
**/
public static void setRetryCount(int retryCount) {
IpControl.retryCount = retryCount;
}
/**
* Connects to the receiver by opening a socket connection through the
* IP and port defined on constructor.
**/
public boolean connectSocket() {
return connectSocket(receiverIP, receiverPort, connectionCheckActive);
}
/**
* Connects to the receiver by opening a socket connection through the
* IP and port.
**/
public boolean connectSocket(String ip, int port, Boolean doConnectionCheck) {
if (ipControlSocket == null || !connected || !ipControlSocket.isConnected()) {
try {
// Creating a socket to connect to the server
ipControlSocket = new Socket();
ipControlSocket.connect(new InetSocketAddress(ip, port), CONNECTION_TIMEOUT);
logger.debug("Connected to {} on port {}", ip, port);
// Get Input and Output streams
outStream = new DataOutputStream(ipControlSocket.getOutputStream());
inStream = new DataInputStream(ipControlSocket.getInputStream());
inBufferedReader = new BufferedReader(new InputStreamReader(inStream));
ipControlSocket.setSoTimeout(SOCKET_TIMEOUT);
outStream.flush();
connected = true;
receiverIP = ip;
receiverPort = port;
// start status update listener
if (dataListener == null) {
dataListener = new DataListener();
dataListener.start();
}
// start connection tester when enabled
if (doConnectionCheck == true) {
logger.debug("conn check enabled, starting hypervisor");
if (connectionSupervisor == null) {
connectionSupervisor = new ConnectionSupervisor(CONNECTION_TEST_INTERVAL);
}
} else {
logger.debug("conn check disabled, not starting hypervisor");
}
} catch (UnknownHostException unknownHost) {
logger.error("You are trying to connect to an unknown host!", unknownHost);
} catch (IOException ioException) {
logger.error("Can't connect " + ip + ":" + port + ": " + ioException.getMessage());
}
}
return connected;
}
/**
* Closes the socket connection.
*
* @return true if the closed successfully
**/
public boolean closeSocket() {
try {
if (dataListener != null) {
dataListener.setInterrupted(true);
dataListener = null;
logger.debug("closed data listener!");
}
if (connectionSupervisor != null) {
connectionSupervisor.stopConnectionTester();
connectionSupervisor = null;
logger.debug("closed connection tester!");
}
if (inStream != null) {
inBufferedReader.close();
inStream.close();
inStream = null;
inBufferedReader = null;
logger.debug("closed input stream!");
}
if (outStream != null) {
outStream.close();
outStream = null;
logger.debug("closed output stream!");
}
if (ipControlSocket != null) {
ipControlSocket.close();
ipControlSocket = null;
logger.debug("closed socket!");
}
connected = false;
} catch (IOException ioException) {
logger.error("Closing connection throws an exception!", ioException);
}
return connected;
}
/**
* Sends to command to the receiver.
* It does not wait for a reply.
*
* @param ipCmd the command to send.
**/
public void sendCommand(String ipCmd) {
logger.debug("Send command: {}", ipCmd);
StringBuilder sb = new StringBuilder();
sb.append(ipCmd);
sb.append('\r');
sendCommand(sb, false, retryCount);
// when comamnd is power on, send it twice. first wakes up receiver
if (ipCmd.contentEquals(IpControlCommand.POWER_ON.getCommand())) {
// delay and repeat command after 100ms - accoring to Pioneer protocol spec
try {
TimeUnit.MILLISECONDS.sleep(100); // wait 100ms to power on
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
sendCommand(sb, false, retryCount);
}
}
/**
* Sends to command to the receiver.
*
* @param ipCmd the Pioneer IP command to send.
* @param closeSocket flag to close the connection when done or leave it open.
* @param retry retry count.
**/
private void sendCommand(StringBuilder ipCmd, boolean closeSocket, int retry) {
if (connectSocket()) {
try {
if (logger.isTraceEnabled()) {
logger.trace("Sending {} bytes: {}", ipCmd.length(),
DatatypeConverter.printHexBinary(ipCmd.toString().getBytes()));
}
outStream.writeBytes(ipCmd.toString());
outStream.flush();
} catch (IOException ioException) {
logger.error("Error occured when sending command", ioException);
if (retry > 0) {
logger.debug("Retry {}...", retry);
closeSocket();
sendCommand(ipCmd, closeSocket, retry--);
}
}
}
// finally close the socket if required ...
if (closeSocket) {
closeSocket();
}
}
/**
* This method wait any state messages form receiver.
*
* @throws IOException
* @throws InterruptedException
* @throws IpcontrolException
**/
private void waitStateMessages()
throws NumberFormatException, IOException, InterruptedException, IpcontrolException {
if (connected) {
PioneerAvrStatusUpdateEvent event = new PioneerAvrStatusUpdateEvent(this);
logger.trace("Waiting status messages");
while (true) {
String receivedData = inBufferedReader.readLine();
if (logger.isTraceEnabled()) {
logger.trace("Received {} bytes: {}", receivedData.length(),
DatatypeConverter.printHexBinary(receivedData.getBytes()));
}
// send message to event listeners
try {
Iterator<PioneerAvrEventListener> iterator = _listeners.iterator();
while (iterator.hasNext()) {
iterator.next().statusUpdateReceived(event, receiverIP, receivedData);
}
} catch (Exception e) {
logger.error("Event listener invoking error", e);
}
}
} else {
throw new IOException("Not Connected to Receiver");
}
}
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("Data listener started");
boolean restartConnection = false;
// as long as no interrupt is requested, continue running
while (!interrupted) {
try {
waitStateMessages();
} catch (IpcontrolException e) {
logger.error("Error occured during message waiting", e);
} catch (SocketTimeoutException e) {
if (connectionCheckActive == true) {
logger.error("No data received during supervision interval ({} msec)!", SOCKET_TIMEOUT);
restartConnection = true;
}
} catch (Exception e) {
if (interrupted != true && this.isInterrupted() != true) {
logger.error("Error occured during message waiting", e);
restartConnection = true;
// sleep a while, to prevent fast looping if error situation
// is permanent
mysleep(1000);
}
}
if (restartConnection) {
restartConnection = false;
closeSocket();
// reopen connection
logger.debug("Reconnecting...");
try {
connected = false;
connectSocket();
} catch (Exception ex) {
logger.error("Reconnection invoking error", ex);
}
}
}
logger.debug("Data listener stopped");
}
private void mysleep(long milli) {
try {
sleep(milli);
} catch (InterruptedException e) {
interrupted = true;
}
}
}
private class ConnectionSupervisor {
private Timer timer;
public ConnectionSupervisor(int milliseconds) {
logger.debug("Connection supervisor started, interval {} milliseconds", milliseconds);
timer = new Timer();
timer.schedule(new Task(), milliseconds, milliseconds);
}
public void stopConnectionTester() {
timer.cancel();
}
class Task extends TimerTask {
@Override
public void run() {
if (connected) {
logger.debug("Test connection to {}:{}", receiverIP, receiverPort);
sendCommand("?P");
}
}
}
}
}