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.heos.internal.resources;
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;
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;
40 * The {@link Telnet} is a Telnet Client which handles the connection
41 * to a network via the Telnet interface
43 * @author Johannes Einig - Initial contribution
46 private final Logger logger = LoggerFactory.getLogger(Telnet.class);
48 private static final int READ_TIMEOUT = 3000;
49 private static final int IS_ALIVE_TIMEOUT = 10000;
51 private final HeosStringPropertyChangeListener eolNotifier = new HeosStringPropertyChangeListener();
52 private final TelnetClient client = new TelnetClient();
53 private ExecutorService timedReaderExecutor;
58 private String readResult = "";
60 private InetAddress address;
61 private DataOutputStream outStream;
62 private BufferedInputStream bufferedStream;
65 * Connects to a host with the specified IP address and port
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
73 public boolean connect(String ip, int port) throws SocketException, IOException {
77 address = InetAddress.getByName(ip);
78 } catch (UnknownHostException e) {
79 logger.debug("Unknown Host Exception - Message: {}", e.getMessage());
81 timedReaderExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("heos-telnet-reader", true));
83 return openConnection();
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();
95 * Appends \r\n to the command.
96 * For clear send use sendClear
98 * @param command The command to be send
99 * @return true after the command was send
100 * @throws IOException
102 public boolean send(String command) throws IOException {
103 if (client.isConnected()) {
104 sendClear(command + "\r\n");
112 * Send command without additional commands
114 * @param command The command to be send
115 * @throws IOException
117 private void sendClear(String command) throws IOException {
118 if (!client.isConnected()) {
122 outStream.writeBytes(command);
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
132 * @return A list with all read commands
133 * @throws ReadException
134 * @throws IOException
135 * @see Telnet.readLine(int timeOut).
137 public String readLine() throws ReadException, IOException {
138 return readLine(READ_TIMEOUT);
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
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
153 public @Nullable String readLine(int timeOut) throws ReadException, IOException {
154 if (client.isConnected()) {
156 return timedCallable(() -> {
157 BufferedReader reader = new CRLFLineReader(
158 new InputStreamReader(bufferedStream, StandardCharsets.UTF_8));
161 lastLine = reader.readLine();
162 } while (reader.ready());
165 } catch (InterruptedException | TimeoutException e) {
166 throw new ReadException(e);
167 } catch (ExecutionException e) {
168 Throwable cause = e.getCause();
169 if (cause instanceof IOException) {
170 throw (IOException) cause;
172 throw new ReadException(cause);
179 private String timedCallable(Callable<String> callable, int timeOut)
180 throws InterruptedException, ExecutionException, TimeoutException {
181 Future<String> future = timedReaderExecutor.submit(callable);
183 return future.get(timeOut, TimeUnit.MILLISECONDS);
184 } catch (Exception e) {
191 * Disconnect Telnet and close all Streams
193 * @throws IOException
195 public void disconnect() throws IOException {
197 timedReaderExecutor.shutdown();
201 * Input Listener which fires event if input is detected
203 public void startInputListener() {
204 logger.debug("Starting input listener");
205 client.setReaderThread(true);
206 client.registerInputListener(this::inputAvailableRead);
209 public void stopInputListener() {
210 logger.debug("Stopping input listener");
211 client.unregisterInputListener();
215 * Reader for InputListenerOnly which only reads the
216 * available data without any check
218 private void inputAvailableRead() {
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());
231 * Read values until end of line is reached.
232 * Then fires event for change Listener.
234 * @return -1 to indicate that end of line is reached
237 private int concatReadResult(String value) {
238 readResult = readResult.concat(value);
239 if (readResult.contains("\r\n")) {
240 eolNotifier.setValue(readResult.trim());
248 * Checks if the HEOS system is reachable
249 * via the network. This does not check if
250 * a Telnet connection is open.
252 * @return true if HEOS is reachable
254 public boolean isHostReachable() {
256 return address != null && address.isReachable(IS_ALIVE_TIMEOUT);
257 } catch (IOException e) {
258 logger.debug("IO Exception- Message: {}", e.getMessage());
264 public String toString() {
265 return "Telnet{" + "ip='" + ip + '\'' + ", port=" + port + '}';
268 public HeosStringPropertyChangeListener getReadResultListener() {
272 public boolean isConnected() {
273 return client.isConnected();
276 public static class ReadException extends Exception {
277 private static final long serialVersionUID = 1L;
279 public ReadException() {
280 super("Can not read from client");
283 public ReadException(Throwable cause) {
284 super("Can not read from client", cause);