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