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