]> git.basschouten.com Git - openhab-addons.git/blob
0e4ea47c091ed0dab4d144ee9f2a2f5c5cf90fd8
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.ecotouch.internal;
14
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.io.InputStreamReader;
19 import java.net.MalformedURLException;
20 import java.net.URL;
21 import java.net.URLConnection;
22 import java.net.URLEncoder;
23 import java.util.*;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 /**
33  * Network communication with Waterkotte EcoTouch heat pumps.
34  *
35  * The communication protocol was reverse engineered from the Easy-Con Android
36  * app. The meaning of the EcoTouch tags was provided by Waterkotte's technical
37  * service (by an excerpt of a developer manual).
38  *
39  * @author Sebastian Held <sebastian.held@gmx.de> - Initial contribution
40  * @since 1.5.0
41  */
42
43 @NonNullByDefault
44 public class EcoTouchConnector {
45     private String ip;
46     private String username;
47     private String password;
48     @Nullable
49     List<String> cookies;
50     static Pattern responsePattern = Pattern.compile("#(.+)\\s+S_OK[^0-9-]+([0-9-]+)\\s+([0-9-.]+)");
51
52     private final Logger logger = LoggerFactory.getLogger(EcoTouchConnector.class);
53
54     /**
55      * Create a network communication without having a current access token.
56      */
57     public EcoTouchConnector(String ip, String username, String password) {
58         this.ip = ip;
59         this.username = username;
60         this.password = password;
61         this.cookies = null;
62     }
63
64     /**
65      * Create a network communication with access token. This speeds up
66      * retrieving values, because the log in step is omitted.
67      */
68     public EcoTouchConnector(String ip, String username, String password, List<String> cookies) {
69         this.ip = ip;
70         this.username = username;
71         this.password = password;
72         this.cookies = cookies;
73     }
74
75     private synchronized void trylogin(boolean force) throws Exception {
76         if (!force && cookies != null) {
77             // we've a login token already
78             return;
79         }
80         login();
81     }
82
83     private void login() throws IOException {
84         cookies = null;
85         String url = null;
86         String line2 = null;
87         String cause = null;
88         try {
89             url = "http://" + ip + "/cgi/login?username=" + URLEncoder.encode(username, "UTF-8") + "&password="
90                     + URLEncoder.encode(password, "UTF-8");
91             URL loginurl = new URL(url);
92             URLConnection connection = loginurl.openConnection();
93             cookies = connection.getHeaderFields().get("Set-Cookie");
94             BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
95             in.readLine();
96             line2 = in.readLine();
97         } catch (MalformedURLException e) {
98             cause = e.toString();
99         } catch (Exception e) {
100             cause = e.toString();
101         }
102
103         if (line2 != null && line2.trim().equals("#E_USER_DONT_EXIST")) {
104             throw new IOException("Username does not exist.");
105         }
106         if (line2 != null && line2.trim().equals("#E_PASS_DONT_MATCH")) {
107             throw new IOException("Password does not match.");
108         }
109         if (line2 != null && line2.trim().equals("#E_TOO_MANY_USERS")) {
110             throw new IOException("Too many users already logged in.");
111         }
112         if (cookies == null) {
113             if (cause == null)
114                 throw new IOException("Cannot login");
115             else
116                 throw new IOException("Cannot login: " + cause);
117         }
118     }
119
120     public void logout() {
121         if (cookies != null) {
122             try {
123                 URL logouturl = new URL("http://" + ip + "/cgi/logout");
124                 logouturl.openConnection();
125             } catch (Exception e) {
126             }
127             cookies = null;
128         }
129     }
130
131     /**
132      * Request a value from the heat pump
133      * 
134      * @param tag
135      *            The register to query (e.g. "A1")
136      * @return value This value is a 16-bit integer.
137      */
138     public String getValue(String tag) throws Exception {
139         Map<String, String> result = getValues(Set.of(tag));
140         String value = result.get(tag);
141         if (value == null) {
142             // failed
143             logger.debug("Cannot get value for tag '{}' from Waterkotte EcoTouch.", tag);
144             throw new EcoTouchException("invalid response from EcoTouch");
145         }
146
147         return value;
148     }
149
150     /**
151      * Request multiple values from the heat pump
152      * 
153      * @param tags
154      *            The registers to query (e.g. "A1")
155      * @return values A map of tags and their respective string values
156      */
157     public Map<String, String> getValues(Set<String> tags) throws Exception {
158         final Integer maxNum = 100;
159
160         Map<String, String> result = new HashMap<String, String>();
161         Integer counter = 1;
162         StringBuilder query = new StringBuilder();
163         Iterator<String> iter = tags.iterator();
164         while (iter.hasNext()) {
165             query.append(String.format("t%d=%s&", counter, iter.next()));
166             counter++;
167             if (counter > maxNum) {
168                 query.deleteCharAt(query.length() - 1); // remove last '&'
169                 String queryStr = String.format("http://%s/cgi/readTags?n=%d&", ip, maxNum) + query;
170                 result.putAll(getValues(queryStr));
171                 counter = 1;
172                 query = new StringBuilder();
173             }
174         }
175
176         if (query.length() > 0) {
177             query.deleteCharAt(query.length() - 1); // remove last '&'
178             String queryStr = String.format("http://%s/cgi/readTags?n=%d&", ip, counter - 1) + query;
179             result.putAll(getValues(queryStr));
180         }
181
182         return result;
183     }
184
185     /**
186      * Send a request to the heat pump and evaluate the result
187      * 
188      * @param url
189      *            The URL to connect to
190      * @return values A map of tags and their respective string values
191      */
192     private Map<String, String> getValues(String url) throws Exception {
193         trylogin(false);
194         Map<String, String> result = new HashMap<String, String>();
195         int loginAttempt = 0;
196         while (loginAttempt < 2) {
197             BufferedReader reader = null;
198             try {
199                 URLConnection connection = new URL(url).openConnection();
200                 var localCookies = cookies;
201                 if (localCookies != null) {
202                     for (String cookie : localCookies) {
203                         connection.addRequestProperty("Cookie", cookie.split(";", 2)[0]);
204                     }
205                 }
206                 InputStream response = connection.getInputStream();
207                 reader = new BufferedReader(new InputStreamReader(response));
208                 // the answer is s.th. like
209                 // #A30 S_OK
210                 // 192 223
211                 // [...]
212                 String line;
213                 while ((line = reader.readLine()) != null) {
214                     String line2 = reader.readLine();
215                     if (line2 == null)
216                         break;
217                     String doubleline = line + "\n" + line2;
218                     Matcher m = responsePattern.matcher(doubleline);
219                     if (m.find()) {
220                         String tag = m.group(1);
221                         String value = m.group(3).trim();
222                         result.put(tag, value);
223                     }
224                 }
225
226                 if (result.isEmpty()) {
227                     // s.th. went wrong; try to log in again
228                     throw new EcoTouchException();
229                 }
230
231                 // succeeded
232                 break;
233             } catch (Exception e) {
234                 if (loginAttempt == 0) {
235                     // try to login once
236                     trylogin(true);
237                 }
238                 loginAttempt++;
239             } finally {
240                 if (reader != null)
241                     reader.close();
242             }
243         }
244
245         return result;
246     }
247
248     /**
249      * Set a value
250      * 
251      * @param tag
252      *            The register to set (e.g. "A1")
253      * @param value
254      *            The 16-bit integer to set the register to
255      * @return value This value is a 16-bit integer.
256      */
257     public int setValue(String tag, int value) throws Exception {
258         trylogin(false);
259
260         // set value
261         String url = "http://" + ip + "/cgi/writeTags?returnValue=true&n=1&t1=" + tag + "&v1=" + value;
262         StringBuilder body = null;
263         int loginAttempt = 0;
264         while (loginAttempt < 2) {
265             BufferedReader reader = null;
266             try {
267                 URLConnection connection = new URL(url).openConnection();
268                 var localCookies = cookies;
269                 if (localCookies != null) {
270                     for (String cookie : localCookies) {
271                         connection.addRequestProperty("Cookie", cookie.split(";", 2)[0]);
272                     }
273                 }
274                 InputStream response = connection.getInputStream();
275                 reader = new BufferedReader(new InputStreamReader(response));
276                 body = new StringBuilder();
277                 String line;
278                 while ((line = reader.readLine()) != null) {
279                     body.append(line + "\n");
280                 }
281                 if (body.toString().contains("#" + tag)) {
282                     // succeeded
283                     break;
284                 }
285                 // s.th. went wrong; try to log in
286                 throw new EcoTouchException();
287             } catch (Exception e) {
288                 if (loginAttempt == 0) {
289                     // try to login once
290                     trylogin(true);
291                 }
292                 loginAttempt++;
293             } finally {
294                 if (reader != null)
295                     reader.close();
296             }
297         }
298
299         if (body == null || !body.toString().contains("#" + tag)) {
300             // failed
301             logger.debug("Cannot get value for tag '{}' from Waterkotte EcoTouch.", tag);
302             throw new EcoTouchException("invalid response from EcoTouch");
303         }
304
305         // ok, the body now contains s.th. like
306         // #A30 S_OK
307         // 192 223
308
309         Matcher m = responsePattern.matcher(body.toString());
310         boolean b = m.find();
311         if (!b) {
312             // ill formatted response
313             logger.debug("ill formatted response: '{}'", body);
314             throw new EcoTouchException("invalid response from EcoTouch");
315         }
316
317         logger.debug("response: '{}'", body.toString());
318         return Integer.parseInt(m.group(3));
319     }
320
321     /**
322      * Authentication token. Store this and use it, when creating the next
323      * instance of this class.
324      * 
325      * @return cookies: This includes the authentication token retrieved during
326      *         log in.
327      */
328     @Nullable
329     public List<String> getCookies() {
330         return cookies;
331     }
332 }