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 DEVICE_ADDRESS = 1;
46 private static final int CONNECTION_TIMEOUT = 1000; // ms
49 public void testForCommands() throws UnknownHostException, SolarMaxException {
50 List<String> validCommands = new ArrayList<>();
51 List<String> commandsToCheck = new ArrayList<String>();
52 List<String> failedCommands = new ArrayList<>();
53 int failedCommandRetry = 0;
54 String lastFailedCommand = "";
56 for (String first : getCharacters()) {
57 for (String second : getCharacters()) {
58 for (String third : getCharacters()) {
59 commandsToCheck.add(first + second + third);
61 // specifically searching for "E" errors with 4 characters (I know now that they don't exist ;-)
62 // commandsToCheck.add("E" + first + second + third);
64 commandsToCheck.add("E" + first + second);
68 // if you only want to try specific commands, perhaps because they failed in the big run, comment out the above
69 // and use this instead
70 // commandsToCheck.addAll(Arrays.asList("RH1", "RH2", "RH3", "TP1", "TP2", "TP3", "UL1", "UL2", "UL3", "UMX",
71 // "UM1", "UM2", "UM3", "UPD", "TCP"));
73 while (!commandsToCheck.isEmpty()) {
74 if (commandsToCheck.size() % 100 == 0) {
75 LOGGER.debug(commandsToCheck.size() + " left to check");
78 if (checkIsValidCommand(commandsToCheck.get(0))) {
79 validCommands.add(commandsToCheck.get(0));
80 commandsToCheck.remove(0);
82 commandsToCheck.remove(0);
84 } catch (Exception e) {
85 LOGGER.debug("Sleeping after Exception: " + e.getLocalizedMessage());
87 if (lastFailedCommand.equals(commandsToCheck.get(0))) {
88 failedCommandRetry = failedCommandRetry + 1;
89 if (failedCommandRetry >= 5) {
90 failedCommands.add(commandsToCheck.get(0));
91 commandsToCheck.remove(0);
94 failedCommandRetry = 0;
95 lastFailedCommand = commandsToCheck.get(0);
98 // Backoff somewhat nicely
99 Thread.sleep(2 * failedCommandRetry * failedCommandRetry * failedCommandRetry);
100 } catch (InterruptedException e1) {
106 } catch (InterruptedException e1) {
111 LOGGER.info("\nValid commands:");
113 for (String validCommand : validCommands) {
114 LOGGER.info(validCommand);
117 LOGGER.info("\nFailed commands:");
119 for (String failedCommand : failedCommands) {
120 LOGGER.info(failedCommand + "\", \"");
124 private boolean checkIsValidCommand(String command)
125 throws InterruptedException, UnknownHostException, SolarMaxException {
126 List<String> commands = new ArrayList<String>();
127 commands.add(command);
129 Map<String, @Nullable String> responseMap = null;
131 responseMap = getValuesFromSolarMax(HOST, PORT, DEVICE_ADDRESS, commands);
133 if (responseMap.containsKey(command)) {
134 LOGGER.debug("Request: " + command + " Valid Response: " + responseMap.get(command));
141 * based on SolarMaxConnector.getValuesFromSolarMax
143 private static Map<String, @Nullable String> getValuesFromSolarMax(final String host, final int portNumber,
144 final int deviceAddress, final List<String> commandList) throws SolarMaxException {
147 Map<String, @Nullable String> returnMap = new HashMap<>();
149 // SolarMax can't answer correclty if too many commands are send in a single request, so limit it to 16 at a
151 int maxConcurrentCommands = 16;
152 int requestsRequired = (commandList.size() / maxConcurrentCommands);
153 if (commandList.size() % maxConcurrentCommands != 0) {
154 requestsRequired = requestsRequired + 1;
156 for (int requestNumber = 0; requestNumber < requestsRequired; requestNumber++) {
157 LOGGER.debug(" Requesting data from {}:{} with timeout of {}ms", host, portNumber, CONNECTION_TIMEOUT);
159 int firstCommandNumber = requestNumber * maxConcurrentCommands;
160 int lastCommandNumber = (requestNumber + 1) * maxConcurrentCommands;
161 if (lastCommandNumber > commandList.size()) {
162 lastCommandNumber = commandList.size();
164 List<String> commandsToSend = commandList.subList(firstCommandNumber, lastCommandNumber);
167 socket = getSocketConnection(host, portNumber);
168 } catch (UnknownHostException e) {
169 throw new SolarMaxConnectionException(e);
171 returnMap.putAll(getValuesFromSolarMax(socket, deviceAddress, commandsToSend));
173 // SolarMax can't deal with requests too close to one another, so just wait a moment
176 } catch (InterruptedException e) {
183 private static Map<String, @Nullable String> getValuesFromSolarMax(final Socket socket, final int deviceAddress,
184 final List<String> commandList) throws SolarMaxException {
185 OutputStream outputStream = null;
186 InputStream inputStream = null;
188 outputStream = socket.getOutputStream();
189 inputStream = socket.getInputStream();
191 return getValuesFromSolarMax(outputStream, inputStream, deviceAddress, commandList);
192 } catch (final SolarMaxException | IOException e) {
193 throw new SolarMaxException("Error getting input/output streams from socket", e);
197 if (outputStream != null) {
198 outputStream.close();
200 if (inputStream != null) {
203 } catch (final IOException e) {
204 // ignore the error, we're dying anyway...
209 private List<String> getCharacters() {
210 List<String> characters = new ArrayList<>();
211 for (char c = 'a'; c <= 'z'; c++) {
212 characters.add(Character.toString(c));
214 for (char c = 'A'; c <= 'Z'; c++) {
215 characters.add(Character.toString(c));
235 private static Socket getSocketConnection(final String host, int portNumber)
236 throws SolarMaxConnectionException, UnknownHostException {
237 portNumber = (portNumber == 0) ? PORT : portNumber;
242 socket = new Socket();
243 LOGGER.debug(" Connecting to " + host + ":" + portNumber + " with a timeout of " + CONNECTION_TIMEOUT);
244 socket.connect(new InetSocketAddress(host, portNumber), CONNECTION_TIMEOUT);
245 LOGGER.debug(" Connected.");
246 socket.setSoTimeout(CONNECTION_TIMEOUT);
247 } catch (final UnknownHostException e) {
249 } catch (final IOException e) {
250 throw new SolarMaxConnectionException(
251 "Error connecting to port '" + portNumber + "' on host '" + host + "'", e);
257 private static Map<String, @Nullable String> getValuesFromSolarMax(final OutputStream outputStream,
258 final InputStream inputStream, final int deviceAddress, final List<String> commandList)
259 throws SolarMaxException {
260 Map<String, @Nullable String> returnedValues;
261 String commandString = getCommandString(commandList);
262 String request = SolarMaxConnector.contructRequest(deviceAddress, commandString);
264 LOGGER.trace(" ==>: {}", request);
266 outputStream.write(request.getBytes());
268 String response = "";
269 byte[] responseByte = new byte[1];
271 // get everything from the stream
273 // read one byte from the stream
274 int bytesRead = inputStream.read(responseByte);
276 // if there was nothing left, break
281 // add the received byte to the response
282 final String responseString = new String(responseByte);
283 response = response + responseString;
285 // if it was the final expected character "}", break
286 if ("}".equals(responseString)) {
291 LOGGER.trace(" <==: {}", response);
293 // if (!validateResponse(response)) {
294 // throw new SolarMaxException("Invalid response received: " + response);
297 returnedValues = extractValuesFromResponse(response);
299 return returnedValues;
300 } catch (IOException e) {
301 LOGGER.debug("Error communicating via input/output streams: {} ", e.getMessage());
302 throw new SolarMaxException(e);
306 static String getCommandString(List<String> commandList) {
307 String commandString = "";
308 for (String command : commandList) {
309 if (!commandString.isEmpty()) {
310 commandString = commandString + ";";
312 commandString = commandString + command;
314 return commandString;
318 * @param response e.g.
319 * "{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}"
320 * @return a map of keys and values
322 static Map<String, @Nullable String> extractValuesFromResponse(String response) {
323 final Map<String, @Nullable String> responseMap = new HashMap<>();
325 // in case there is no response
326 if (response.indexOf("|") == -1) {
327 LOGGER.warn("Response doesn't contain data. Response: {}", response);
331 // extract the body first
332 // start by getting the part of the response between the two pipes
333 String body = response.substring(response.indexOf("|") + 1, response.lastIndexOf("|"));
335 // the name/value pairs now lie after the ":"
336 body = body.substring(body.indexOf(":") + 1);
338 // split into an array of name=value pairs
339 String[] entries = body.split(";");
340 for (String entry : entries) {
342 if (entry.length() != 0) {
343 // could be split on "=" instead of fixed length or made to respect length of command, but they're all 3
344 // characters long (then plus "=")
345 String responseKey = entry.substring(0, 3);
347 String responseValue = (entry.length() >= 5) ? entry.substring(4) : null;
349 responseMap.put(responseKey, responseValue);