2 * Copyright (c) 2010-2022 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.networkupstools.internal.nut;
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.io.PrintWriter;
19 import java.net.InetSocketAddress;
20 import java.net.Socket;
21 import java.nio.charset.StandardCharsets;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
29 * Connector class manages the socket connection to the NUT server.
31 * @author Hilbrand Bouwkamp - Initial contribution
36 private static final String USERNAME = "USERNAME %s";
37 private static final String PASSWORD = "PASSWORD %s";
38 private static final String OK = "OK";
39 private static final String ERR = "ERR";
40 private static final int TIME_OUT_MILLISECONDS = 10_000;
41 private static final int MAX_RETRIES = 3;
43 private final Logger logger = LoggerFactory.getLogger(NutConnector.class);
45 private final String login;
46 private final String password;
47 private final InetSocketAddress inetSocketAddress;
48 private @Nullable Socket socket;
49 private @Nullable BufferedReader reader;
50 private @Nullable PrintWriter writer;
57 * @param username username
58 * @param password password
60 NutConnector(final String host, final int port, final String username, final String password) {
61 this.login = username.isEmpty() ? "" : String.format(USERNAME, username);
62 this.password = password.isEmpty() ? "" : String.format(PASSWORD, password);
63 inetSocketAddress = new InetSocketAddress(host, port);
67 * Communicates to read the data to the NUT server. It handles the connection and authentication. Sends the command
68 * to the NUT server and passes the reading of the values to the readFunction argument.
70 * @param <R> The type of the returned data
71 * @param command The command to send to the NUT server
72 * @param readFunction Function called to handle the lines read from the NUT server
73 * @return the data read from the NUT server
74 * @throws NutException Exception thrown related to the NUT server connection and/or data.
76 public synchronized <R> R read(final String command,
77 final NutFunction<NutSupplier<String>, @Nullable R> readFunction) throws NutException {
83 final PrintWriter localWriter = writer;
84 final BufferedReader localReader = reader;
86 if (localWriter == null) {
87 throw new NutException("Writer closed.");
89 localWriter.println(command);
91 if (localReader == null) {
92 throw new NutException("Reader closed.");
94 return readFunction.apply(() -> readLine(localReader));
96 } catch (final NutException | RuntimeException e) {
99 if (retry < MAX_RETRIES) {
100 logger.debug("Error during command retry {}:", retry, e);
109 * Opens a Socket connection if there is no connection or if the connection is closed. Authenticates if
110 * username/password is provided.
112 * @throws NutException Exception thrown if no connection to NUT server could be made successfully.
114 public void connectIfClosed() throws NutException {
115 if (socket == null || socket.isClosed() || !socket.isConnected()) {
118 socket = newSocket();
119 socket.connect(inetSocketAddress, TIME_OUT_MILLISECONDS);
120 final BufferedReader localReader = new BufferedReader(
121 new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
122 reader = localReader;
123 final PrintWriter localWriter = new PrintWriter(socket.getOutputStream(), true);
124 writer = localWriter;
125 writeCommand(login, localReader, localWriter);
126 writeCommand(password, localReader, localWriter);
127 } catch (final IOException e) {
128 throw new NutException(e);
136 public synchronized void close() {
138 if (socket != null) {
142 } catch (final IOException e) {
143 logger.debug("Closing socket failed", e);
147 private void closeStreams() {
149 if (reader != null && socket != null && !socket.isInputShutdown()) {
152 if (writer != null && socket != null && !socket.isOutputShutdown()) {
155 } catch (final IOException e) {
156 logger.debug("Closing streams failed", e);
161 * Protected method to be able to mock the Socket connection in unit tests.
163 * @return a new Socket object
165 protected Socket newSocket() {
169 private @Nullable String readLine(final BufferedReader reader) throws NutException {
171 final String line = reader.readLine();
173 if (line != null && line.startsWith(ERR)) {
174 throw new NutException(line);
177 } catch (final IOException e) {
178 throw new NutException(e);
182 private void writeCommand(final String argument, final BufferedReader reader, final PrintWriter writer)
183 throws IOException, NutException {
184 if (!argument.isEmpty()) {
185 writer.println(argument);
186 final String result = reader.readLine();
188 logger.trace("Command result: {}", result);
189 if (result == null) {
190 throw new NutException("No data read after sending command");
191 } else if (!result.startsWith(OK)) {
192 throw new NutException(result);