/** * 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.onkyo.internal.eiscp; import java.io.DataInputStream; import java.io.IOException; import java.io.ObjectOutputStream; 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 javax.xml.bind.DatatypeConverter; import org.apache.commons.lang.StringUtils; import org.openhab.binding.onkyo.internal.OnkyoEventListener; import org.openhab.binding.onkyo.internal.OnkyoStatusUpdateEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <p> * <b>Note:</b>This class was originally developed by Tom Gutwin but suffered heavy * refactoring recently. * </p> * <br> * * A class that wraps the communication to Onkyo/Integra devices using the * Ethernet Integra Serial Control Protocol (eISCP). * * @author Tom Gutwin P.Eng * @author Thomas.Eichstaedt-Engelen (Refactoring) * @author Pauli Anttila (Simplified, rewritten and added status update listener functionality) */ public class Eiscp implements EiscpInterface { private static final Logger logger = LoggerFactory.getLogger(Eiscp.class); /** Instantiated class IP for the receiver to communicate with. **/ private String receiverIP = ""; /** default eISCP port. **/ public static final int DEFAULT_EISCP_PORT = 60128; /** 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_EISCP_PORT; private Socket eiscpSocket = null; private DataListener dataListener = null; private ObjectOutputStream outStream = null; private DataInputStream inStream = null; private boolean connected = false; private List<OnkyoEventListener> _listeners = new ArrayList<OnkyoEventListener>(); private int retryCount = 1; private ConnectionSupervisor connectionSupervisor = null; /** * Constructor that takes your receivers IP and port. **/ public Eiscp(String ip, int eiscpPort) { if (StringUtils.isNotBlank(ip)) { receiverIP = ip; } if (eiscpPort >= 1) { receiverPort = eiscpPort; } } /** * Add event listener, which will be invoked when status upadte is received from receiver. **/ @Override public synchronized void addEventListener(OnkyoEventListener listener) { _listeners.add(listener); } /** * Remove event listener. **/ @Override public synchronized void removeEventListener(OnkyoEventListener listener) { _listeners.remove(listener); } /** * Get retry count value. **/ @Override public int getRetryCount() { return retryCount; } /** * Set retry count value. How many times command is retried when error occurs. **/ @Override public void setRetryCount(int retryCount) { this.retryCount = retryCount; } /** * Connects to the receiver by opening a socket connection through the * IP and port defined on constructor. **/ @Override public boolean connectSocket() { return connectSocket(receiverIP, receiverPort); } /** * Connects to the receiver by opening a socket connection through the * IP and port. **/ public boolean connectSocket(String ip, int port) { if (eiscpSocket == null || !connected || !eiscpSocket.isConnected()) { try { // Creating a socket to connect to the server eiscpSocket = new Socket(); eiscpSocket.connect(new InetSocketAddress(ip, port), CONNECTION_TIMEOUT); logger.debug("Connected to {} on port {}", ip, port); // Get Input and Output streams outStream = new ObjectOutputStream(eiscpSocket.getOutputStream()); inStream = new DataInputStream(eiscpSocket.getInputStream()); eiscpSocket.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 if (connectionSupervisor == null) { connectionSupervisor = new ConnectionSupervisor(CONNECTION_TEST_INTERVAL); } } catch (UnknownHostException unknownHost) { logger.error("You are trying to connect to an unknown host!", unknownHost); } catch (IOException ioException) { logger.error("Can't connect: " + ioException.getMessage()); } } return connected; } /** * Closes the socket connection. * * @return true if the closed successfully **/ @Override 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) { inStream.close(); inStream = null; logger.debug("closed input stream!"); } if (outStream != null) { outStream.close(); outStream = null; logger.debug("closed output stream!"); } if (eiscpSocket != null) { eiscpSocket.close(); eiscpSocket = null; logger.debug("closed socket!"); } connected = false; } catch (IOException ioException) { logger.error("Closing connection throws an exception!", ioException); } return connected; } /** * Wraps a command in a eiscp data message (data characters). * * @param eiscpCmd * eISCP command. * @return StringBuffer holing the full iscp message packet **/ private StringBuilder getEiscpMessage(String eiscpCmd) { StringBuilder sb = new StringBuilder(); int eiscpDataSize = 2 + eiscpCmd.length() + 1; // this is the eISCP data size /* * This is where I construct the entire message character by character. * Each char is represented by a 2 digit hex value */ sb.append("ISCP"); // the following are all in HEX representing one char // 4 char Big Endian Header sb.append((char) 0x00); sb.append((char) 0x00); sb.append((char) 0x00); sb.append((char) 0x10); // 4 char Big Endian data size sb.append((char) ((eiscpDataSize >> 24) & 0xFF)); sb.append((char) ((eiscpDataSize >> 16) & 0xFF)); sb.append((char) ((eiscpDataSize >> 8) & 0xFF)); sb.append((char) (eiscpDataSize & 0xFF)); // eiscp_version = "01"; sb.append((char) 0x01); // 3 chars reserved = "00"+"00"+"00"; sb.append((char) 0x00); sb.append((char) 0x00); sb.append((char) 0x00); // eISCP data // Start Character sb.append("!"); // eISCP data - unit type char '1' is receiver sb.append("1"); // eISCP data - 3 char command and param ie PWR01 sb.append(eiscpCmd); // msg end - EOF sb.append((char) 0x0D); return sb; } /** * Sends to command to the receiver. * It does not wait for a reply. * * @param eiscpCmd the eISCP command to send. **/ @Override public void sendCommand(String eiscpCmd) { logger.debug("Send command: {} to {}:{} ({})", eiscpCmd, receiverIP, receiverPort, eiscpSocket); StringBuilder sb = getEiscpMessage(eiscpCmd); sendCommand(sb, false, retryCount); } /** * Sends to command to the receiver and close the connection when done. * It does not wait for a reply. * * @param eiscpCmd the eISCP command to send. **/ public void sendCommandAndClose(String eiscpCmd) { logger.debug("Send command: {}", eiscpCmd); StringBuilder sb = getEiscpMessage(eiscpCmd); sendCommand(sb, true, retryCount); } /** * Sends to command to the receiver. * * @param eiscpCmd the eISCP command to send. * @param closeSocket flag to close the connection when done or leave it open. * @param retry retry count. **/ private void sendCommand(StringBuilder eiscpCmd, boolean closeSocket, int retry) { if (connectSocket()) { try { if (logger.isTraceEnabled()) { logger.trace("Sending {} bytes: {}", eiscpCmd.length(), DatatypeConverter.printHexBinary(eiscpCmd.toString().getBytes())); } outStream.writeBytes(eiscpCmd.toString()); outStream.flush(); } catch (IOException ioException) { logger.error("Error occured when sending command", ioException); if (retry > 0) { logger.debug("Retry {}...", retry); closeSocket(); sendCommand(eiscpCmd, closeSocket, retry--); } } } // finally close the socket if required ... if (closeSocket) { closeSocket(); } } /** * This method wait any state messages form receiver. * * @throws IOException * @throws InterruptedException * @throws EiscpException **/ private void waitStateMessages() throws NumberFormatException, IOException, InterruptedException, EiscpException { if (connected) { OnkyoStatusUpdateEvent event = new OnkyoStatusUpdateEvent(this); logger.trace("Waiting status messages"); while (true) { // 1st 4 chars are the leadIn if (inStream.readByte() != 'I') { continue; } if (inStream.readByte() != 'S') { continue; } if (inStream.readByte() != 'C') { continue; } if (inStream.readByte() != 'P') { continue; } // header size final int headerSize = (inStream.readByte() & 0xFF) << 24 | (inStream.readByte() & 0xFF) << 16 | (inStream.readByte() & 0xFF) << 8 | (inStream.readByte() & 0xFF); logger.trace("Header size: {}", headerSize); if (headerSize != 16) { throw new EiscpException("Unsupported header size: " + headerSize); } // header size final int dataSize = (inStream.readByte() & 0xFF) << 24 | (inStream.readByte() & 0xFF) << 16 | (inStream.readByte() & 0xFF) << 8 | (inStream.readByte() & 0xFF); logger.trace("Data size: {}", dataSize); // version final byte versionChar = inStream.readByte(); if (versionChar != 1) { throw new EiscpException("Unsupported version " + String.valueOf(versionChar)); } // skip 3 reserved bytes inStream.readByte(); inStream.readByte(); inStream.readByte(); byte[] data = new byte[dataSize]; final int bytesReceived = inStream.read(data, 0, data.length); if (logger.isTraceEnabled()) { logger.trace("Received {} bytes: {}", bytesReceived, DatatypeConverter.printHexBinary(data)); } if (bytesReceived != dataSize) { throw new EiscpException("Data missing: " + (dataSize - bytesReceived)); } // start char final byte startChar = data[0]; if (startChar != '!') { throw new EiscpException("Illegal start char " + startChar); } // unit type @SuppressWarnings("unused") final byte unitType = data[1]; // data should be end to "[EOF]" or "[EOF][CR]" or // "[EOF][CR][LF]" characters depend on model // [EOF] End of File ASCII Code 0x1A // [CR] Carriage Return ASCII Code 0x0D (\r) // [LF] Line Feed ASCII Code 0x0A (\n) int endBytes = 0; if (data[dataSize - 4] == (byte) 0x1A && data[dataSize - 3] == '\r' && data[dataSize - 2] == '\n' && data[dataSize - 1] == 0x00) { // skip "[EOF][CR][LF][NULL]" endBytes = 4; } else if (data[dataSize - 3] == (byte) 0x1A && data[dataSize - 2] == '\r' && data[dataSize - 1] == '\n') { // skip "[EOF][CR][LF]" endBytes = 3; } else if (data[dataSize - 2] == (byte) 0x1A && data[dataSize - 1] == '\r') { // "[EOF][CR]" endBytes = 2; } else if (data[dataSize - 1] == (byte) 0x1A) { // "[EOF]" endBytes = 1; } else { throw new EiscpException("Illegal end of message"); } int bytesToCopy = dataSize - 2 - endBytes; byte[] message = new byte[bytesToCopy]; // skip 2 first bytes and copy all bytes before end bytes System.arraycopy(data, 2, message, 0, bytesToCopy); // send message to event listeners try { Iterator<OnkyoEventListener> iterator = _listeners.iterator(); while (iterator.hasNext()) { iterator.next().statusUpdateReceived(event, receiverIP, new String(message)); } } 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 (EiscpException e) { logger.error("Error occured during message waiting", e); } catch (SocketTimeoutException e) { logger.error("No data received during supervision interval ({} sec)!", 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; // 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("PWRQSTN"); } } } } }