]> git.basschouten.com Git - openhab-addons.git/blob
84c6024dbefbca1d4635d804890e7b8290fd49a1
[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.solarmax.internal.connector;
14
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;
24 import java.util.Map;
25
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;
31
32 /**
33  * The {@link SolarmaxConnectorFindCommands} class wass used to brute-force detect different replies from the SolarMax
34  * device
35  *
36  * @author Jamie Townsend - Initial contribution
37  */
38 @NonNullByDefault
39 public class SolarmaxConnectorFindCommands {
40
41     private static final Logger LOGGER = LoggerFactory.getLogger(SolarMaxConnector.class);
42
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
46
47     @Test
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 = "";
54
55         for (String first : getCharacters()) {
56             for (String second : getCharacters()) {
57                 for (String third : getCharacters()) {
58                     commandsToCheck.add(first + second + third);
59
60                     // specifically searching for "E" errors with 4 characters (I know now that they don't exist ;-)
61                     // commandsToCheck.add("E" + first + second + third);
62                 }
63                 commandsToCheck.add("E" + first + second);
64             }
65         }
66
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"));
71
72         while (!commandsToCheck.isEmpty()) {
73             if (commandsToCheck.size() % 100 == 0) {
74                 LOGGER.debug(commandsToCheck.size() + " left to check");
75             }
76             try {
77                 if (checkIsValidCommand(commandsToCheck.get(0))) {
78                     validCommands.add(commandsToCheck.get(0));
79                     commandsToCheck.remove(0);
80                 } else {
81                     commandsToCheck.remove(0);
82                 }
83             } catch (Exception e) {
84                 LOGGER.debug("Sleeping after Exception: " + e.getLocalizedMessage());
85
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);
91                     }
92                 } else {
93                     failedCommandRetry = 0;
94                     lastFailedCommand = commandsToCheck.get(0);
95                 }
96                 try {
97                     // Backoff somewhat nicely
98                     Thread.sleep(2 * failedCommandRetry * failedCommandRetry * failedCommandRetry);
99                 } catch (InterruptedException e1) {
100                     // do nothing
101                 }
102             }
103             try {
104                 Thread.sleep(10);
105             } catch (InterruptedException e1) {
106                 // do nothing
107             }
108         }
109
110         LOGGER.info("\nValid commands:");
111
112         for (String validCommand : validCommands) {
113             LOGGER.info(validCommand);
114         }
115
116         LOGGER.info("\nFailed commands:");
117
118         for (String failedCommand : failedCommands) {
119             LOGGER.info(failedCommand + "\", \"");
120         }
121     }
122
123     private boolean checkIsValidCommand(String command)
124             throws InterruptedException, UnknownHostException, SolarMaxException {
125         List<String> commands = new ArrayList<String>();
126         commands.add(command);
127
128         Map<String, @Nullable String> responseMap = null;
129
130         responseMap = getValuesFromSolarMax(HOST, PORT, commands);
131
132         if (responseMap.containsKey(command)) {
133             LOGGER.debug("Request: " + command + " Valid Response: " + responseMap.get(command));
134             return true;
135         }
136         return false;
137     }
138
139     /**
140      * based on SolarMaxConnector.getValuesFromSolarMax
141      */
142     private static Map<String, @Nullable String> getValuesFromSolarMax(final String host, int port,
143             final List<String> commandList) throws SolarMaxException {
144         Socket socket;
145
146         Map<String, @Nullable String> returnMap = new HashMap<>();
147
148         // SolarMax can't answer correclty if too many commands are send in a single request, so limit it to 16 at a
149         // time
150         int maxConcurrentCommands = 16;
151         int requestsRequired = (commandList.size() / maxConcurrentCommands);
152         if (commandList.size() % maxConcurrentCommands != 0) {
153             requestsRequired = requestsRequired + 1;
154         }
155         for (int requestNumber = 0; requestNumber < requestsRequired; requestNumber++) {
156             LOGGER.debug("    Requesting data from {}:{} with timeout of {}ms", host, port, CONNECTION_TIMEOUT);
157
158             int firstCommandNumber = requestNumber * maxConcurrentCommands;
159             int lastCommandNumber = (requestNumber + 1) * maxConcurrentCommands;
160             if (lastCommandNumber > commandList.size()) {
161                 lastCommandNumber = commandList.size();
162             }
163             List<String> commandsToSend = commandList.subList(firstCommandNumber, lastCommandNumber);
164
165             try {
166                 socket = getSocketConnection(host, port);
167             } catch (UnknownHostException e) {
168                 throw new SolarMaxConnectionException(e);
169             }
170             returnMap.putAll(getValuesFromSolarMax(socket, commandsToSend));
171
172             // SolarMax can't deal with requests too close to one another, so just wait a moment
173             try {
174                 Thread.sleep(10);
175             } catch (InterruptedException e) {
176                 // do nothing
177             }
178         }
179         return returnMap;
180     }
181
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;
186         try {
187             outputStream = socket.getOutputStream();
188             inputStream = socket.getInputStream();
189
190             return getValuesFromSolarMax(outputStream, inputStream, commandList);
191         } catch (final SolarMaxException | IOException e) {
192             throw new SolarMaxException("Error getting input/output streams from socket", e);
193         } finally {
194             try {
195                 socket.close();
196                 if (outputStream != null) {
197                     outputStream.close();
198                 }
199                 if (inputStream != null) {
200                     inputStream.close();
201                 }
202             } catch (final IOException e) {
203                 // ignore the error, we're dying anyway...
204             }
205         }
206     }
207
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));
212         }
213         for (char c = 'A'; c <= 'Z'; c++) {
214             characters.add(Character.toString(c));
215         }
216         characters.add("0");
217         characters.add("1");
218         characters.add("2");
219         characters.add("3");
220         characters.add("4");
221         characters.add("5");
222         characters.add("6");
223         characters.add("7");
224         characters.add("8");
225         characters.add("9");
226
227         characters.add(".");
228         characters.add("-");
229         characters.add("_");
230
231         return characters;
232     }
233
234     private static Socket getSocketConnection(final String host, int port)
235             throws SolarMaxConnectionException, UnknownHostException {
236         port = (port == 0) ? SolarmaxConnectorFindCommands.PORT : port;
237
238         Socket socket;
239
240         try {
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) {
247             throw e;
248         } catch (final IOException e) {
249             throw new SolarMaxConnectionException("Error connecting to port '" + port + "' on host '" + host + "'", e);
250         }
251
252         return socket;
253     }
254
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);
260         try {
261             LOGGER.trace("    ==>: {}", request);
262
263             outputStream.write(request.getBytes());
264
265             String response = "";
266             byte[] responseByte = new byte[1];
267
268             // get everything from the stream
269             while (true) {
270                 // read one byte from the stream
271                 int bytesRead = inputStream.read(responseByte);
272
273                 // if there was nothing left, break
274                 if (bytesRead < 1) {
275                     break;
276                 }
277
278                 // add the received byte to the response
279                 final String responseString = new String(responseByte);
280                 response = response + responseString;
281
282                 // if it was the final expected character "}", break
283                 if ("}".equals(responseString)) {
284                     break;
285                 }
286             }
287
288             LOGGER.trace("    <==: {}", response);
289
290             // if (!validateResponse(response)) {
291             // throw new SolarMaxException("Invalid response received: " + response);
292             // }
293
294             returnedValues = extractValuesFromResponse(response);
295
296             return returnedValues;
297         } catch (IOException e) {
298             LOGGER.debug("Error communicating via input/output streams: {} ", e.getMessage());
299             throw new SolarMaxException(e);
300         }
301     }
302
303     static String getCommandString(List<String> commandList) {
304         String commandString = "";
305         for (String command : commandList) {
306             if (!commandString.isEmpty()) {
307                 commandString = commandString + ";";
308             }
309             commandString = commandString + command;
310         }
311         return commandString;
312     }
313
314     /**
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
318      */
319     static Map<String, @Nullable String> extractValuesFromResponse(String response) {
320         final Map<String, @Nullable String> responseMap = new HashMap<>();
321
322         // in case there is no response
323         if (response.indexOf("|") == -1) {
324             LOGGER.warn("Response doesn't contain data. Response: {}", response);
325             return responseMap;
326         }
327
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("|"));
331
332         // the name/value pairs now lie after the ":"
333         body = body.substring(body.indexOf(":") + 1);
334
335         // split into an array of name=value pairs
336         String[] entries = body.split(";");
337         for (String entry : entries) {
338
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);
343
344                 String responseValue = (entry.length() >= 5) ? entry.substring(4) : null;
345
346                 responseMap.put(responseKey, responseValue);
347             }
348         }
349
350         return responseMap;
351     }
352 }