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