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.solarmax.internal.connector;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
18 import java.net.InetSocketAddress;
19 import java.net.Socket;
20 import java.net.UnknownHostException;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.List;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.junit.jupiter.api.Test;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
33 * The {@link SolarmaxConnectorFindCommands} class wass used to brute-force detect different replies from the SolarMax
36 * @author Jamie Townsend - Initial contribution
39 public class SolarmaxConnectorFindCommands {
41 private static final Logger LOGGER = LoggerFactory.getLogger(SolarMaxConnector.class);
43 private static final String HOST = "192.168.1.151";
44 private static final int PORT = 12345;
45 private static final int CONNECTION_TIMEOUT = 1000; // ms
48 public void testForCommands() throws UnknownHostException, SolarMaxException {
49 List<String> validCommands = new ArrayList<>();
50 List<String> commandsToCheck = new ArrayList<String>();
51 List<String> failedCommands = new ArrayList<>();
52 int failedCommandRetry = 0;
53 String lastFailedCommand = "";
55 for (String first : getCharacters()) {
56 for (String second : getCharacters()) {
57 for (String third : getCharacters()) {
58 commandsToCheck.add(first + second + third);
60 // specifically searching for "E" errors with 4 characters (I know now that they don't exist ;-)
61 // commandsToCheck.add("E" + first + second + third);
63 commandsToCheck.add("E" + first + second);
67 // if you only want to try specific commands, perhaps because they failed in the big run, comment out the above
68 // and use this instead
69 // commandsToCheck.addAll(Arrays.asList("RH1", "RH2", "RH3", "TP1", "TP2", "TP3", "UL1", "UL2", "UL3", "UMX",
70 // "UM1", "UM2", "UM3", "UPD", "TCP"));
72 while (!commandsToCheck.isEmpty()) {
73 if (commandsToCheck.size() % 100 == 0) {
74 LOGGER.debug(commandsToCheck.size() + " left to check");
77 if (checkIsValidCommand(commandsToCheck.get(0))) {
78 validCommands.add(commandsToCheck.get(0));
79 commandsToCheck.remove(0);
81 commandsToCheck.remove(0);
83 } catch (Exception e) {
84 LOGGER.debug("Sleeping after Exception: " + e.getLocalizedMessage());
86 if (lastFailedCommand.equals(commandsToCheck.get(0))) {
87 failedCommandRetry = failedCommandRetry + 1;
88 if (failedCommandRetry >= 5) {
89 failedCommands.add(commandsToCheck.get(0));
90 commandsToCheck.remove(0);
93 failedCommandRetry = 0;
94 lastFailedCommand = commandsToCheck.get(0);
97 // Backoff somewhat nicely
98 Thread.sleep(2 * failedCommandRetry * failedCommandRetry * failedCommandRetry);
99 } catch (InterruptedException e1) {
105 } catch (InterruptedException e1) {
110 LOGGER.info("\nValid commands:");
112 for (String validCommand : validCommands) {
113 LOGGER.info(validCommand);
116 LOGGER.info("\nFailed commands:");
118 for (String failedCommand : failedCommands) {
119 LOGGER.info(failedCommand + "\", \"");
123 private boolean checkIsValidCommand(String command)
124 throws InterruptedException, UnknownHostException, SolarMaxException {
125 List<String> commands = new ArrayList<String>();
126 commands.add(command);
128 Map<String, @Nullable String> responseMap = null;
130 responseMap = getValuesFromSolarMax(HOST, PORT, commands);
132 if (responseMap.containsKey(command)) {
133 LOGGER.debug("Request: " + command + " Valid Response: " + responseMap.get(command));
140 * based on SolarMaxConnector.getValuesFromSolarMax
142 private static Map<String, @Nullable String> getValuesFromSolarMax(final String host, int port,
143 final List<String> commandList) throws SolarMaxException {
146 Map<String, @Nullable String> returnMap = new HashMap<>();
148 // SolarMax can't answer correclty if too many commands are send in a single request, so limit it to 16 at a
150 int maxConcurrentCommands = 16;
151 int requestsRequired = (commandList.size() / maxConcurrentCommands);
152 if (commandList.size() % maxConcurrentCommands != 0) {
153 requestsRequired = requestsRequired + 1;
155 for (int requestNumber = 0; requestNumber < requestsRequired; requestNumber++) {
156 LOGGER.debug(" Requesting data from {}:{} with timeout of {}ms", host, port, CONNECTION_TIMEOUT);
158 int firstCommandNumber = requestNumber * maxConcurrentCommands;
159 int lastCommandNumber = (requestNumber + 1) * maxConcurrentCommands;
160 if (lastCommandNumber > commandList.size()) {
161 lastCommandNumber = commandList.size();
163 List<String> commandsToSend = commandList.subList(firstCommandNumber, lastCommandNumber);
166 socket = getSocketConnection(host, port);
167 } catch (UnknownHostException e) {
168 throw new SolarMaxConnectionException(e);
170 returnMap.putAll(getValuesFromSolarMax(socket, commandsToSend));
172 // SolarMax can't deal with requests too close to one another, so just wait a moment
175 } catch (InterruptedException e) {
182 private static Map<String, @Nullable String> getValuesFromSolarMax(final Socket socket,
183 final List<String> commandList) throws SolarMaxException {
184 OutputStream outputStream = null;
185 InputStream inputStream = null;
187 outputStream = socket.getOutputStream();
188 inputStream = socket.getInputStream();
190 return getValuesFromSolarMax(outputStream, inputStream, commandList);
191 } catch (final SolarMaxException | IOException e) {
192 throw new SolarMaxException("Error getting input/output streams from socket", e);
196 if (outputStream != null) {
197 outputStream.close();
199 if (inputStream != null) {
202 } catch (final IOException e) {
203 // ignore the error, we're dying anyway...
208 private List<String> getCharacters() {
209 List<String> characters = new ArrayList<>();
210 for (char c = 'a'; c <= 'z'; c++) {
211 characters.add(Character.toString(c));
213 for (char c = 'A'; c <= 'Z'; c++) {
214 characters.add(Character.toString(c));
234 private static Socket getSocketConnection(final String host, int port)
235 throws SolarMaxConnectionException, UnknownHostException {
236 port = (port == 0) ? SolarmaxConnectorFindCommands.PORT : port;
241 socket = new Socket();
242 LOGGER.debug(" Connecting to " + host + ":" + port + " with a timeout of " + CONNECTION_TIMEOUT);
243 socket.connect(new InetSocketAddress(host, port), CONNECTION_TIMEOUT);
244 LOGGER.debug(" Connected.");
245 socket.setSoTimeout(CONNECTION_TIMEOUT);
246 } catch (final UnknownHostException e) {
248 } catch (final IOException e) {
249 throw new SolarMaxConnectionException("Error connecting to port '" + port + "' on host '" + host + "'", e);
255 private static Map<String, @Nullable String> getValuesFromSolarMax(final OutputStream outputStream,
256 final InputStream inputStream, final List<String> commandList) throws SolarMaxException {
257 Map<String, @Nullable String> returnedValues;
258 String commandString = getCommandString(commandList);
259 String request = SolarMaxConnector.contructRequest(commandString);
261 LOGGER.trace(" ==>: {}", request);
263 outputStream.write(request.getBytes());
265 String response = "";
266 byte[] responseByte = new byte[1];
268 // get everything from the stream
270 // read one byte from the stream
271 int bytesRead = inputStream.read(responseByte);
273 // if there was nothing left, break
278 // add the received byte to the response
279 final String responseString = new String(responseByte);
280 response = response + responseString;
282 // if it was the final expected character "}", break
283 if ("}".equals(responseString)) {
288 LOGGER.trace(" <==: {}", response);
290 // if (!validateResponse(response)) {
291 // throw new SolarMaxException("Invalid response received: " + response);
294 returnedValues = extractValuesFromResponse(response);
296 return returnedValues;
297 } catch (IOException e) {
298 LOGGER.debug("Error communicating via input/output streams: {} ", e.getMessage());
299 throw new SolarMaxException(e);
303 static String getCommandString(List<String> commandList) {
304 String commandString = "";
305 for (String command : commandList) {
306 if (!commandString.isEmpty()) {
307 commandString = commandString + ";";
309 commandString = commandString + command;
311 return commandString;
315 * @param response e.g.
316 * "{01;FB;6D|64:KDY=82;KMT=8F;KYR=23F7;KT0=72F1;TNF=1386;TKK=28;PAC=1F70;PRL=28;IL1=236;UL1=8F9;SYS=4E28,0|19E5}"
317 * @return a map of keys and values
319 static Map<String, @Nullable String> extractValuesFromResponse(String response) {
320 final Map<String, @Nullable String> responseMap = new HashMap<>();
322 // in case there is no response
323 if (response.indexOf("|") == -1) {
324 LOGGER.warn("Response doesn't contain data. Response: {}", response);
328 // extract the body first
329 // start by getting the part of the response between the two pipes
330 String body = response.substring(response.indexOf("|") + 1, response.lastIndexOf("|"));
332 // the name/value pairs now lie after the ":"
333 body = body.substring(body.indexOf(":") + 1);
335 // split into an array of name=value pairs
336 String[] entries = body.split(";");
337 for (String entry : entries) {
339 if (entry.length() != 0) {
340 // could be split on "=" instead of fixed length or made to respect length of command, but they're all 3
341 // characters long (then plus "=")
342 String responseKey = entry.substring(0, 3);
344 String responseValue = (entry.length() >= 5) ? entry.substring(4) : null;
346 responseMap.put(responseKey, responseValue);