]> git.basschouten.com Git - openhab-addons.git/blob
c433fcbb17e6661a9881f24793042cbb711f70f9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.onkyo.internal;
14
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;
26
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;
34
35 /**
36  * This class open a TCP/IP connection to the Onkyo device and send a command.
37  *
38  * @author Pauli Anttila - Initial contribution
39  */
40 public class OnkyoConnection {
41
42     private final Logger logger = LoggerFactory.getLogger(OnkyoConnection.class);
43
44     /** default eISCP port. **/
45     public static final int DEFAULT_EISCP_PORT = 60128;
46
47     /** Connection timeout in milliseconds **/
48     private static final int CONNECTION_TIMEOUT = 5000;
49
50     /** Connection test interval in milliseconds **/
51     private static final int CONNECTION_TEST_INTERVAL = 60000;
52
53     /** Socket timeout in milliseconds **/
54     private static final int SOCKET_TIMEOUT = CONNECTION_TEST_INTERVAL + 10000;
55
56     /** Connection retry count on error situations **/
57     private static final int FAST_CONNECTION_RETRY_COUNT = 3;
58
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;
62
63     private String ip;
64     private int port;
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;
73
74     public OnkyoConnection(String ip) {
75         this.ip = ip;
76         this.port = DEFAULT_EISCP_PORT;
77     }
78
79     public OnkyoConnection(String ip, int port) {
80         this.ip = ip;
81         this.port = port;
82     }
83
84     /**
85      * Open connection to the Onkyo device.
86      */
87     public void openConnection() {
88         connectSocket();
89     }
90
91     /**
92      * Closes the connection to the Onkyo device.
93      */
94     public void closeConnection() {
95         closeSocket();
96     }
97
98     public void addEventListener(OnkyoEventListener listener) {
99         this.listeners.add(listener);
100     }
101
102     public void removeEventListener(OnkyoEventListener listener) {
103         this.listeners.remove(listener);
104     }
105
106     public String getConnectionName() {
107         return ip + ":" + port;
108     }
109
110     public boolean isConnected() {
111         return connected;
112     }
113
114     /**
115      * Sends a command to Onkyo device.
116      *
117      * @param cmd eISCP command to send
118      */
119     public void send(final String cmd, final String value) {
120         try {
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);
124         }
125     }
126
127     private void sendCommand(EiscpMessage msg) {
128         logger.debug("Send command: {} to {}:{} ({})", msg.toString(), ip, port, eiscpSocket);
129         sendCommand(msg, retryCount);
130     }
131
132     /**
133      * Sends to command to the receiver.
134      *
135      * @param eiscpCmd the eISCP command to send.
136      * @param retry retry count when connection fails.
137      */
138     private void sendCommand(EiscpMessage msg, int retry) {
139         if (connectSocket()) {
140             try {
141                 String data = EiscpProtocol.createEiscpPdu(msg);
142                 if (logger.isTraceEnabled()) {
143                     logger.trace("Sending {} bytes: {}", data.length(), HexUtils.bytesToHex(data.getBytes()));
144                 }
145
146                 outStream.writeBytes(data);
147                 outStream.flush();
148             } catch (IOException ioException) {
149                 logger.warn("Error occurred when sending command: {}", ioException.getMessage());
150
151                 if (retry > 0) {
152                     logger.debug("Retry {}...", retry);
153                     closeSocket();
154                     sendCommand(msg, retry - 1);
155                 } else {
156                     sendConnectionErrorEvent(ioException.getMessage());
157                 }
158             }
159         }
160     }
161
162     /**
163      * Connects to the receiver by opening a socket connection through the
164      * IP and port.
165      */
166     private synchronized boolean connectSocket() {
167         if (eiscpSocket == null || !connected || !eiscpSocket.isConnected()) {
168             try {
169                 // Creating a socket to connect to the server
170                 eiscpSocket = new Socket();
171
172                 // start connection tester
173                 if (connectionSupervisor == null) {
174                     connectionSupervisor = new ConnectionSupervisor(CONNECTION_TEST_INTERVAL);
175                 }
176
177                 eiscpSocket.connect(new InetSocketAddress(ip, port), CONNECTION_TIMEOUT);
178
179                 logger.debug("Connected to {}:{}", ip, port);
180
181                 // Get Input and Output streams
182                 outStream = new DataOutputStream(eiscpSocket.getOutputStream());
183                 inStream = new DataInputStream(eiscpSocket.getInputStream());
184
185                 eiscpSocket.setSoTimeout(SOCKET_TIMEOUT);
186                 outStream.flush();
187                 connected = true;
188
189                 // start status update listener
190                 if (dataListener == null) {
191                     dataListener = new DataListener();
192                     dataListener.start();
193                 }
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());
200             }
201         }
202
203         return connected;
204     }
205
206     /**
207      * Closes the socket connection.
208      *
209      * @return true if the closed successfully
210      */
211     private boolean closeSocket() {
212         try {
213             if (dataListener != null) {
214                 dataListener.setInterrupted(true);
215                 dataListener = null;
216                 logger.debug("closed data listener!");
217             }
218             if (connectionSupervisor != null) {
219                 connectionSupervisor.stopConnectionTester();
220                 connectionSupervisor = null;
221                 logger.debug("closed connection tester!");
222             }
223             if (inStream != null) {
224                 try {
225                     inStream.close();
226                 } catch (IOException e) {
227                 }
228                 inStream = null;
229                 logger.debug("closed input stream!");
230             }
231             if (outStream != null) {
232                 try {
233                     outStream.close();
234                 } catch (IOException e) {
235                 }
236                 outStream = null;
237                 logger.debug("closed output stream!");
238             }
239             if (eiscpSocket != null) {
240                 try {
241                     eiscpSocket.close();
242                 } catch (IOException e) {
243                 }
244                 eiscpSocket = null;
245                 logger.debug("closed socket!");
246             }
247             connected = false;
248         } catch (Exception e) {
249             logger.debug("Closing connection throws an exception, {}", e.getMessage());
250         }
251
252         return connected;
253     }
254
255     /**
256      * This method wait any state messages form receiver.
257      *
258      * @throws IOException
259      * @throws InterruptedException
260      * @throws EiscpException
261      */
262     private void waitStateMessages() throws NumberFormatException, IOException, InterruptedException, EiscpException {
263         if (connected) {
264             logger.trace("Waiting status messages");
265
266             while (true) {
267                 EiscpMessage message = EiscpProtocol.getNextMessage(inStream);
268                 sendMessageEvent(message);
269             }
270         } else {
271             throw new IOException("Not Connected to Receiver");
272         }
273     }
274
275     private class DataListener extends Thread {
276         private boolean interrupted = false;
277
278         DataListener() {
279         }
280
281         public void setInterrupted(boolean interrupted) {
282             this.interrupted = interrupted;
283             this.interrupt();
284         }
285
286         @Override
287         public void run() {
288             logger.debug("Data listener started");
289
290             boolean restartConnection = false;
291             long connectionAttempts = 0;
292
293             // as long as no interrupt is requested, continue running
294             while (!interrupted) {
295                 try {
296                     waitStateMessages();
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;
307
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);
311                         } else {
312                             // slow down after few faster attempts
313                             if (connectionAttempts == FAST_CONNECTION_RETRY_COUNT) {
314                                 logger.debug(
315                                         "Connection failed {} times to {}:{}, slowing down automatic connection to {} seconds.",
316                                         FAST_CONNECTION_RETRY_COUNT, ip, port, SLOW_CONNECTION_RETRY_DELAY / 1000);
317                             }
318                             mysleep(SLOW_CONNECTION_RETRY_DELAY);
319                         }
320                     }
321                 }
322
323                 if (restartConnection) {
324                     restartConnection = false;
325
326                     // reopen connection
327                     logger.debug("Reconnecting...");
328
329                     try {
330                         connected = false;
331                         connectSocket();
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());
338                     }
339                 }
340             }
341
342             logger.debug("Data listener stopped");
343         }
344
345         private void mysleep(long milli) {
346             try {
347                 sleep(milli);
348             } catch (InterruptedException e) {
349                 interrupted = true;
350             }
351         }
352     }
353
354     private class ConnectionSupervisor {
355         private Timer timer;
356
357         public ConnectionSupervisor(int milliseconds) {
358             logger.debug("Connection supervisor started, interval {} milliseconds", milliseconds);
359
360             timer = new Timer();
361             timer.schedule(new Task(), milliseconds, milliseconds);
362         }
363
364         public void stopConnectionTester() {
365             timer.cancel();
366         }
367
368         class Task extends TimerTask {
369             @Override
370             public void run() {
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());
374             }
375         }
376     }
377
378     private void sendConnectionErrorEvent(String errorMsg) {
379         // send message to event listeners
380         try {
381             for (OnkyoEventListener listener : listeners) {
382                 listener.connectionError(ip, errorMsg);
383             }
384         } catch (Exception ex) {
385             logger.debug("Event listener invoking error: {}", ex.getMessage());
386         }
387     }
388
389     private void sendMessageEvent(EiscpMessage message) {
390         // send message to event listeners
391         try {
392             for (OnkyoEventListener listener : listeners) {
393                 listener.statusUpdateReceived(ip, message);
394             }
395         } catch (Exception e) {
396             logger.debug("Event listener invoking error: {}", e.getMessage());
397         }
398     }
399 }