2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.onkyo.internal;
15 import java.io.DataInputStream;
16 import java.io.DataOutputStream;
17 import java.io.IOException;
18 import java.net.InetSocketAddress;
19 import java.net.Socket;
20 import java.net.SocketTimeoutException;
21 import java.net.UnknownHostException;
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.Timer;
25 import java.util.TimerTask;
27 import org.apache.commons.io.IOUtils;
28 import org.openhab.binding.onkyo.internal.eiscp.EiscpCommand;
29 import org.openhab.binding.onkyo.internal.eiscp.EiscpException;
30 import org.openhab.binding.onkyo.internal.eiscp.EiscpMessage;
31 import org.openhab.binding.onkyo.internal.eiscp.EiscpProtocol;
32 import org.openhab.core.util.HexUtils;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * This class open a TCP/IP connection to the Onkyo device and send a command.
39 * @author Pauli Anttila - Initial contribution
41 public class OnkyoConnection {
43 private final Logger logger = LoggerFactory.getLogger(OnkyoConnection.class);
45 /** default eISCP port. **/
46 public static final int DEFAULT_EISCP_PORT = 60128;
48 /** Connection timeout in milliseconds **/
49 private static final int CONNECTION_TIMEOUT = 5000;
51 /** Connection test interval in milliseconds **/
52 private static final int CONNECTION_TEST_INTERVAL = 60000;
54 /** Socket timeout in milliseconds **/
55 private static final int SOCKET_TIMEOUT = CONNECTION_TEST_INTERVAL + 10000;
57 /** Connection retry count on error situations **/
58 private static final int FAST_CONNECTION_RETRY_COUNT = 3;
60 /** Connection retry delays in milliseconds **/
61 private static final int FAST_CONNECTION_RETRY_DELAY = 1000;
62 private static final int SLOW_CONNECTION_RETRY_DELAY = 60000;
66 private Socket eiscpSocket;
67 private DataListener dataListener;
68 private DataOutputStream outStream;
69 private DataInputStream inStream;
70 private boolean connected;
71 private List<OnkyoEventListener> listeners = new ArrayList<>();
72 private int retryCount = 1;
73 private ConnectionSupervisor connectionSupervisor;
75 public OnkyoConnection(String ip) {
77 this.port = DEFAULT_EISCP_PORT;
80 public OnkyoConnection(String ip, int port) {
86 * Open connection to the Onkyo device.
88 public void openConnection() {
93 * Closes the connection to the Onkyo device.
95 public void closeConnection() {
99 public void addEventListener(OnkyoEventListener listener) {
100 this.listeners.add(listener);
103 public void removeEventListener(OnkyoEventListener listener) {
104 this.listeners.remove(listener);
107 public String getConnectionName() {
108 return ip + ":" + port;
111 public boolean isConnected() {
116 * Sends a command to Onkyo device.
118 * @param cmd eISCP command to send
120 public void send(final String cmd, final String value) {
122 sendCommand(new EiscpMessage.MessageBuilder().command(cmd).value(value).build());
123 } catch (Exception e) {
124 logger.warn("Could not send command to device on {}:{}: ", ip, port, e);
128 private void sendCommand(EiscpMessage msg) {
129 logger.debug("Send command: {} to {}:{} ({})", msg.toString(), ip, port, eiscpSocket);
130 sendCommand(msg, retryCount);
134 * Sends to command to the receiver.
136 * @param eiscpCmd the eISCP command to send.
137 * @param retry retry count when connection fails.
139 private void sendCommand(EiscpMessage msg, int retry) {
140 if (connectSocket()) {
142 String data = EiscpProtocol.createEiscpPdu(msg);
143 if (logger.isTraceEnabled()) {
144 logger.trace("Sending {} bytes: {}", data.length(), HexUtils.bytesToHex(data.getBytes()));
147 outStream.writeBytes(data);
149 } catch (IOException ioException) {
150 logger.warn("Error occurred when sending command: {}", ioException.getMessage());
153 logger.debug("Retry {}...", retry);
155 sendCommand(msg, retry - 1);
157 sendConnectionErrorEvent(ioException.getMessage());
164 * Connects to the receiver by opening a socket connection through the
167 private synchronized boolean connectSocket() {
168 if (eiscpSocket == null || !connected || !eiscpSocket.isConnected()) {
170 // Creating a socket to connect to the server
171 eiscpSocket = new Socket();
173 // start connection tester
174 if (connectionSupervisor == null) {
175 connectionSupervisor = new ConnectionSupervisor(CONNECTION_TEST_INTERVAL);
178 eiscpSocket.connect(new InetSocketAddress(ip, port), CONNECTION_TIMEOUT);
180 logger.debug("Connected to {}:{}", ip, port);
182 // Get Input and Output streams
183 outStream = new DataOutputStream(eiscpSocket.getOutputStream());
184 inStream = new DataInputStream(eiscpSocket.getInputStream());
186 eiscpSocket.setSoTimeout(SOCKET_TIMEOUT);
190 // start status update listener
191 if (dataListener == null) {
192 dataListener = new DataListener();
193 dataListener.start();
195 } catch (UnknownHostException unknownHost) {
196 logger.debug("You are trying to connect to an unknown host: {}", unknownHost.getMessage());
197 sendConnectionErrorEvent(unknownHost.getMessage());
198 } catch (IOException ioException) {
199 logger.debug("Can't connect: {}", ioException.getMessage());
200 sendConnectionErrorEvent(ioException.getMessage());
208 * Closes the socket connection.
210 * @return true if the closed successfully
212 private boolean closeSocket() {
214 if (dataListener != null) {
215 dataListener.setInterrupted(true);
217 logger.debug("closed data listener!");
219 if (connectionSupervisor != null) {
220 connectionSupervisor.stopConnectionTester();
221 connectionSupervisor = null;
222 logger.debug("closed connection tester!");
224 if (inStream != null) {
225 IOUtils.closeQuietly(inStream);
227 logger.debug("closed input stream!");
229 if (outStream != null) {
230 IOUtils.closeQuietly(outStream);
232 logger.debug("closed output stream!");
234 if (eiscpSocket != null) {
235 IOUtils.closeQuietly(eiscpSocket);
237 logger.debug("closed socket!");
240 } catch (Exception e) {
241 logger.debug("Closing connection throws an exception, {}", e.getMessage());
248 * This method wait any state messages form receiver.
250 * @throws IOException
251 * @throws InterruptedException
252 * @throws EiscpException
254 private void waitStateMessages() throws NumberFormatException, IOException, InterruptedException, EiscpException {
256 logger.trace("Waiting status messages");
259 EiscpMessage message = EiscpProtocol.getNextMessage(inStream);
260 sendMessageEvent(message);
263 throw new IOException("Not Connected to Receiver");
267 private class DataListener extends Thread {
268 private boolean interrupted = false;
273 public void setInterrupted(boolean interrupted) {
274 this.interrupted = interrupted;
280 logger.debug("Data listener started");
282 boolean restartConnection = false;
283 long connectionAttempts = 0;
285 // as long as no interrupt is requested, continue running
286 while (!interrupted) {
289 connectionAttempts = 0;
290 } catch (EiscpException e) {
291 logger.debug("Error occurred during message waiting: {}", e.getMessage());
292 } catch (SocketTimeoutException e) {
293 logger.debug("No data received during supervision interval ({} ms)!", SOCKET_TIMEOUT);
294 restartConnection = true;
295 } catch (Exception e) {
296 if (!interrupted && !this.isInterrupted()) {
297 logger.debug("Error occurred during message waiting: {}", e.getMessage());
298 restartConnection = true;
300 // sleep a while, to prevent fast looping if error situation is permanent
301 if (++connectionAttempts < FAST_CONNECTION_RETRY_COUNT) {
302 mysleep(FAST_CONNECTION_RETRY_DELAY);
304 // slow down after few faster attempts
305 if (connectionAttempts == FAST_CONNECTION_RETRY_COUNT) {
307 "Connection failed {} times to {}:{}, slowing down automatic connection to {} seconds.",
308 FAST_CONNECTION_RETRY_COUNT, ip, port, SLOW_CONNECTION_RETRY_DELAY / 1000);
310 mysleep(SLOW_CONNECTION_RETRY_DELAY);
315 if (restartConnection) {
316 restartConnection = false;
319 logger.debug("Reconnecting...");
324 logger.debug("Test connection to {}:{}", ip, port);
325 sendCommand(new EiscpMessage.MessageBuilder().command(EiscpCommand.POWER_QUERY.getCommand())
326 .value(EiscpCommand.POWER_QUERY.getValue()).build());
327 } catch (Exception ex) {
328 logger.debug("Reconnection invoking error: {}", ex.getMessage());
329 sendConnectionErrorEvent(ex.getMessage());
334 logger.debug("Data listener stopped");
337 private void mysleep(long milli) {
340 } catch (InterruptedException e) {
346 private class ConnectionSupervisor {
349 public ConnectionSupervisor(int milliseconds) {
350 logger.debug("Connection supervisor started, interval {} milliseconds", milliseconds);
353 timer.schedule(new Task(), milliseconds, milliseconds);
356 public void stopConnectionTester() {
360 class Task extends TimerTask {
363 logger.debug("Test connection to {}:{}", ip, port);
364 sendCommand(new EiscpMessage.MessageBuilder().command(EiscpCommand.POWER_QUERY.getCommand())
365 .value(EiscpCommand.POWER_QUERY.getValue()).build());
370 private void sendConnectionErrorEvent(String errorMsg) {
371 // send message to event listeners
373 for (OnkyoEventListener listener : listeners) {
374 listener.connectionError(ip, errorMsg);
376 } catch (Exception ex) {
377 logger.debug("Event listener invoking error: {}", ex.getMessage());
381 private void sendMessageEvent(EiscpMessage message) {
382 // send message to event listeners
384 for (OnkyoEventListener listener : listeners) {
385 listener.statusUpdateReceived(ip, message);
387 } catch (Exception e) {
388 logger.debug("Event listener invoking error: {}", e.getMessage());