]> git.basschouten.com Git - openhab-addons.git/blob
0749695a453f09d79dd740b1093a8b5b696106fe
[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.heos.internal.resources;
14
15 import java.io.BufferedInputStream;
16 import java.io.BufferedReader;
17 import java.io.DataOutputStream;
18 import java.io.IOException;
19 import java.io.InputStreamReader;
20 import java.net.InetAddress;
21 import java.net.SocketException;
22 import java.net.UnknownHostException;
23 import java.nio.charset.StandardCharsets;
24 import java.util.concurrent.Callable;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.ExecutorService;
27 import java.util.concurrent.Executors;
28 import java.util.concurrent.Future;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.TimeoutException;
31
32 import org.apache.commons.net.io.CRLFLineReader;
33 import org.apache.commons.net.telnet.TelnetClient;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.core.common.NamedThreadFactory;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * The {@link Telnet} is a Telnet Client which handles the connection
41  * to a network via the Telnet interface
42  *
43  * @author Johannes Einig - Initial contribution
44  */
45 public class Telnet {
46     private final Logger logger = LoggerFactory.getLogger(Telnet.class);
47
48     private static final int READ_TIMEOUT = 3000;
49     private static final int IS_ALIVE_TIMEOUT = 10000;
50
51     private final HeosStringPropertyChangeListener eolNotifier = new HeosStringPropertyChangeListener();
52     private final TelnetClient client = new TelnetClient();
53     private ExecutorService timedReaderExecutor;
54
55     private String ip;
56     private int port;
57
58     private String readResult = "";
59
60     private InetAddress address;
61     private DataOutputStream outStream;
62     private BufferedInputStream bufferedStream;
63
64     /**
65      * Connects to a host with the specified IP address and port
66      *
67      * @param ip IP Address of the host
68      * @param port where to be connected
69      * @return True if connection was successful
70      * @throws SocketException
71      * @throws IOException
72      */
73     public boolean connect(String ip, int port) throws SocketException, IOException {
74         this.ip = ip;
75         this.port = port;
76         try {
77             address = InetAddress.getByName(ip);
78         } catch (UnknownHostException e) {
79             logger.debug("Unknown Host Exception - Message: {}", e.getMessage());
80         }
81         timedReaderExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("heos-telnet-reader", true));
82
83         return openConnection();
84     }
85
86     private boolean openConnection() throws IOException {
87         client.setConnectTimeout(5000);
88         client.connect(ip, port);
89         outStream = new DataOutputStream(client.getOutputStream());
90         bufferedStream = new BufferedInputStream(client.getInputStream());
91         return client.isConnected();
92     }
93
94     /**
95      * Appends \r\n to the command.
96      * For clear send use sendClear
97      *
98      * @param command The command to be send
99      * @return true after the command was send
100      * @throws IOException
101      */
102     public boolean send(String command) throws IOException {
103         if (client.isConnected()) {
104             sendClear(command + "\r\n");
105             return true;
106         } else {
107             return false;
108         }
109     }
110
111     /**
112      * Send command without additional commands
113      *
114      * @param command The command to be send
115      * @throws IOException
116      */
117     private void sendClear(String command) throws IOException {
118         if (!client.isConnected()) {
119             return;
120         }
121
122         outStream.writeBytes(command);
123         outStream.flush();
124     }
125
126     /**
127      * Read all commands till an End Of Line is detected
128      * I more than one line is read every line is an
129      * element in the returned {@code ArrayList<>}
130      * Reading timed out after 3000 milliseconds. For another timing
131      *
132      * @return A list with all read commands
133      * @throws ReadException
134      * @throws IOException
135      * @see #readLine(int timeOut)
136      */
137     public String readLine() throws ReadException, IOException {
138         return readLine(READ_TIMEOUT);
139     }
140
141     /**
142      * Read all commands till an End Of Line is detected
143      * I more than one line is read every line is an
144      * element in the returned {@code ArrayList<>}
145      * Reading time out is defined by parameter in
146      * milliseconds.
147      *
148      * @param timeOut the time in millis after reading times out
149      * @return A list with all read commands
150      * @throws ReadException
151      * @throws IOException
152      */
153     public @Nullable String readLine(int timeOut) throws ReadException, IOException {
154         if (client.isConnected()) {
155             try {
156                 return timedCallable(() -> {
157                     BufferedReader reader = new CRLFLineReader(
158                             new InputStreamReader(bufferedStream, StandardCharsets.UTF_8));
159                     String lastLine;
160                     do {
161                         lastLine = reader.readLine();
162                     } while (reader.ready());
163                     return lastLine;
164                 }, timeOut);
165             } catch (InterruptedException | TimeoutException e) {
166                 throw new ReadException(e);
167             } catch (ExecutionException e) {
168                 Throwable cause = e.getCause();
169                 if (cause instanceof IOException exception) {
170                     throw exception;
171                 } else {
172                     throw new ReadException(cause);
173                 }
174             }
175         }
176         return null;
177     }
178
179     private String timedCallable(Callable<String> callable, int timeOut)
180             throws InterruptedException, ExecutionException, TimeoutException {
181         Future<String> future = timedReaderExecutor.submit(callable);
182         try {
183             return future.get(timeOut, TimeUnit.MILLISECONDS);
184         } catch (Exception e) {
185             future.cancel(true);
186             throw e;
187         }
188     }
189
190     /**
191      * Disconnect Telnet and close all Streams
192      *
193      * @throws IOException
194      */
195     public void disconnect() throws IOException {
196         client.disconnect();
197         timedReaderExecutor.shutdown();
198     }
199
200     /**
201      * Input Listener which fires event if input is detected
202      */
203     public void startInputListener() {
204         logger.debug("Starting input listener");
205         client.setReaderThread(true);
206         client.registerInputListener(this::inputAvailableRead);
207     }
208
209     public void stopInputListener() {
210         logger.debug("Stopping input listener");
211         client.unregisterInputListener();
212     }
213
214     /**
215      * Reader for InputListenerOnly which only reads the
216      * available data without any check
217      */
218     private void inputAvailableRead() {
219         try {
220             int i = bufferedStream.available();
221             byte[] buffer = new byte[i];
222             bufferedStream.read(buffer);
223             String str = new String(buffer, StandardCharsets.UTF_8);
224             concatReadResult(str);
225         } catch (IOException e) {
226             logger.debug("IO Exception, message: {}", e.getMessage());
227         }
228     }
229
230     /**
231      * Read values until end of line is reached.
232      * Then fires event for change Listener.
233      *
234      * @return -1 to indicate that end of line is reached
235      *         else returns 0
236      */
237     private int concatReadResult(String value) {
238         readResult = readResult.concat(value);
239         if (readResult.contains("\r\n")) {
240             eolNotifier.setValue(readResult.trim());
241             readResult = "";
242             return -1;
243         }
244         return 0;
245     }
246
247     /**
248      * Checks if the HEOS system is reachable
249      * via the network. This does not check if
250      * a Telnet connection is open.
251      *
252      * @return true if HEOS is reachable
253      */
254     public boolean isHostReachable() {
255         try {
256             return address != null && address.isReachable(IS_ALIVE_TIMEOUT);
257         } catch (IOException e) {
258             logger.debug("IO Exception- Message: {}", e.getMessage());
259             return false;
260         }
261     }
262
263     @Override
264     public String toString() {
265         return "Telnet{" + "ip='" + ip + '\'' + ", port=" + port + '}';
266     }
267
268     public HeosStringPropertyChangeListener getReadResultListener() {
269         return eolNotifier;
270     }
271
272     public boolean isConnected() {
273         return client.isConnected();
274     }
275
276     public static class ReadException extends Exception {
277         private static final long serialVersionUID = 1L;
278
279         public ReadException() {
280             super("Can not read from client");
281         }
282
283         public ReadException(Throwable cause) {
284             super("Can not read from client", cause);
285         }
286     }
287 }