2 * Copyright (c) 2010-2023 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.openhab.binding.onkyo.internal.eiscp.EiscpCommand;
28 import org.openhab.binding.onkyo.internal.eiscp.EiscpException;
29 import org.openhab.binding.onkyo.internal.eiscp.EiscpMessage;
30 import org.openhab.binding.onkyo.internal.eiscp.EiscpProtocol;
31 import org.openhab.core.util.HexUtils;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * This class open a TCP/IP connection to the Onkyo device and send a command.
38 * @author Pauli Anttila - Initial contribution
40 public class OnkyoConnection {
42 private final Logger logger = LoggerFactory.getLogger(OnkyoConnection.class);
44 /** default eISCP port. **/
45 public static final int DEFAULT_EISCP_PORT = 60128;
47 /** Connection timeout in milliseconds **/
48 private static final int CONNECTION_TIMEOUT = 5000;
50 /** Connection test interval in milliseconds **/
51 private static final int CONNECTION_TEST_INTERVAL = 60000;
53 /** Socket timeout in milliseconds **/
54 private static final int SOCKET_TIMEOUT = CONNECTION_TEST_INTERVAL + 10000;
56 /** Connection retry count on error situations **/
57 private static final int FAST_CONNECTION_RETRY_COUNT = 3;
59 /** Connection retry delays in milliseconds **/
60 private static final int FAST_CONNECTION_RETRY_DELAY = 1000;
61 private static final int SLOW_CONNECTION_RETRY_DELAY = 60000;
65 private Socket eiscpSocket;
66 private DataListener dataListener;
67 private DataOutputStream outStream;
68 private DataInputStream inStream;
69 private boolean connected;
70 private List<OnkyoEventListener> listeners = new ArrayList<>();
71 private int retryCount = 1;
72 private ConnectionSupervisor connectionSupervisor;
74 public OnkyoConnection(String ip) {
76 this.port = DEFAULT_EISCP_PORT;
79 public OnkyoConnection(String ip, int port) {
85 * Open connection to the Onkyo device.
87 public void openConnection() {
92 * Closes the connection to the Onkyo device.
94 public void closeConnection() {
98 public void addEventListener(OnkyoEventListener listener) {
99 this.listeners.add(listener);
102 public void removeEventListener(OnkyoEventListener listener) {
103 this.listeners.remove(listener);
106 public String getConnectionName() {
107 return ip + ":" + port;
110 public boolean isConnected() {
115 * Sends a command to Onkyo device.
117 * @param cmd eISCP command to send
119 public void send(final String cmd, final String value) {
121 sendCommand(new EiscpMessage.MessageBuilder().command(cmd).value(value).build());
122 } catch (Exception e) {
123 logger.warn("Could not send command to device on {}:{}: ", ip, port, e);
127 private void sendCommand(EiscpMessage msg) {
128 logger.debug("Send command: {} to {}:{} ({})", msg.toString(), ip, port, eiscpSocket);
129 sendCommand(msg, retryCount);
133 * Sends to command to the receiver.
135 * @param eiscpCmd the eISCP command to send.
136 * @param retry retry count when connection fails.
138 private void sendCommand(EiscpMessage msg, int retry) {
139 if (connectSocket()) {
141 String data = EiscpProtocol.createEiscpPdu(msg);
142 if (logger.isTraceEnabled()) {
143 logger.trace("Sending {} bytes: {}", data.length(), HexUtils.bytesToHex(data.getBytes()));
146 outStream.writeBytes(data);
148 } catch (IOException ioException) {
149 logger.warn("Error occurred when sending command: {}", ioException.getMessage());
152 logger.debug("Retry {}...", retry);
154 sendCommand(msg, retry - 1);
156 sendConnectionErrorEvent(ioException.getMessage());
163 * Connects to the receiver by opening a socket connection through the
166 private synchronized boolean connectSocket() {
167 if (eiscpSocket == null || !connected || !eiscpSocket.isConnected()) {
169 // Creating a socket to connect to the server
170 eiscpSocket = new Socket();
172 // start connection tester
173 if (connectionSupervisor == null) {
174 connectionSupervisor = new ConnectionSupervisor(CONNECTION_TEST_INTERVAL);
177 eiscpSocket.connect(new InetSocketAddress(ip, port), CONNECTION_TIMEOUT);
179 logger.debug("Connected to {}:{}", ip, port);
181 // Get Input and Output streams
182 outStream = new DataOutputStream(eiscpSocket.getOutputStream());
183 inStream = new DataInputStream(eiscpSocket.getInputStream());
185 eiscpSocket.setSoTimeout(SOCKET_TIMEOUT);
189 // start status update listener
190 if (dataListener == null) {
191 dataListener = new DataListener();
192 dataListener.start();
194 } catch (UnknownHostException unknownHost) {
195 logger.debug("You are trying to connect to an unknown host: {}", unknownHost.getMessage());
196 sendConnectionErrorEvent(unknownHost.getMessage());
197 } catch (IOException ioException) {
198 logger.debug("Can't connect: {}", ioException.getMessage());
199 sendConnectionErrorEvent(ioException.getMessage());
207 * Closes the socket connection.
209 * @return true if the closed successfully
211 private boolean closeSocket() {
213 if (dataListener != null) {
214 dataListener.setInterrupted(true);
216 logger.debug("closed data listener!");
218 if (connectionSupervisor != null) {
219 connectionSupervisor.stopConnectionTester();
220 connectionSupervisor = null;
221 logger.debug("closed connection tester!");
223 if (inStream != null) {
226 } catch (IOException e) {
229 logger.debug("closed input stream!");
231 if (outStream != null) {
234 } catch (IOException e) {
237 logger.debug("closed output stream!");
239 if (eiscpSocket != null) {
242 } catch (IOException e) {
245 logger.debug("closed socket!");
248 } catch (Exception e) {
249 logger.debug("Closing connection throws an exception, {}", e.getMessage());
256 * This method wait any state messages form receiver.
258 * @throws IOException
259 * @throws InterruptedException
260 * @throws EiscpException
262 private void waitStateMessages() throws NumberFormatException, IOException, InterruptedException, EiscpException {
264 logger.trace("Waiting status messages");
267 EiscpMessage message = EiscpProtocol.getNextMessage(inStream);
268 sendMessageEvent(message);
271 throw new IOException("Not Connected to Receiver");
275 private class DataListener extends Thread {
276 private boolean interrupted = false;
281 public void setInterrupted(boolean interrupted) {
282 this.interrupted = interrupted;
288 logger.debug("Data listener started");
290 boolean restartConnection = false;
291 long connectionAttempts = 0;
293 // as long as no interrupt is requested, continue running
294 while (!interrupted) {
297 connectionAttempts = 0;
298 } catch (EiscpException e) {
299 logger.debug("Error occurred during message waiting: {}", e.getMessage());
300 } catch (SocketTimeoutException e) {
301 logger.debug("No data received during supervision interval ({} ms)!", SOCKET_TIMEOUT);
302 restartConnection = true;
303 } catch (Exception e) {
304 if (!interrupted && !this.isInterrupted()) {
305 logger.debug("Error occurred during message waiting: {}", e.getMessage());
306 restartConnection = true;
308 // sleep a while, to prevent fast looping if error situation is permanent
309 if (++connectionAttempts < FAST_CONNECTION_RETRY_COUNT) {
310 mysleep(FAST_CONNECTION_RETRY_DELAY);
312 // slow down after few faster attempts
313 if (connectionAttempts == FAST_CONNECTION_RETRY_COUNT) {
315 "Connection failed {} times to {}:{}, slowing down automatic connection to {} seconds.",
316 FAST_CONNECTION_RETRY_COUNT, ip, port, SLOW_CONNECTION_RETRY_DELAY / 1000);
318 mysleep(SLOW_CONNECTION_RETRY_DELAY);
323 if (restartConnection) {
324 restartConnection = false;
327 logger.debug("Reconnecting...");
332 logger.debug("Test connection to {}:{}", ip, port);
333 sendCommand(new EiscpMessage.MessageBuilder().command(EiscpCommand.POWER_QUERY.getCommand())
334 .value(EiscpCommand.POWER_QUERY.getValue()).build());
335 } catch (Exception ex) {
336 logger.debug("Reconnection invoking error: {}", ex.getMessage());
337 sendConnectionErrorEvent(ex.getMessage());
342 logger.debug("Data listener stopped");
345 private void mysleep(long milli) {
348 } catch (InterruptedException e) {
354 private class ConnectionSupervisor {
357 public ConnectionSupervisor(int milliseconds) {
358 logger.debug("Connection supervisor started, interval {} milliseconds", milliseconds);
361 timer.schedule(new Task(), milliseconds, milliseconds);
364 public void stopConnectionTester() {
368 class Task extends TimerTask {
371 logger.debug("Test connection to {}:{}", ip, port);
372 sendCommand(new EiscpMessage.MessageBuilder().command(EiscpCommand.POWER_QUERY.getCommand())
373 .value(EiscpCommand.POWER_QUERY.getValue()).build());
378 private void sendConnectionErrorEvent(String errorMsg) {
379 // send message to event listeners
381 for (OnkyoEventListener listener : listeners) {
382 listener.connectionError(ip, errorMsg);
384 } catch (Exception ex) {
385 logger.debug("Event listener invoking error: {}", ex.getMessage());
389 private void sendMessageEvent(EiscpMessage message) {
390 // send message to event listeners
392 for (OnkyoEventListener listener : listeners) {
393 listener.statusUpdateReceived(ip, message);
395 } catch (Exception e) {
396 logger.debug("Event listener invoking error: {}", e.getMessage());