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.ecotouch.internal;
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;
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;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * Network communication with Waterkotte EcoTouch heat pumps.
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).
43 * @author Sebastian Held <sebastian.held@gmx.de> - Initial contribution
48 public class EcoTouchConnector {
50 private String username;
51 private String password;
54 static Pattern responsePattern = Pattern.compile("#(.+)\\s+S_OK[^0-9-]+([0-9-]+)\\s+([0-9-.]+)");
56 private final Logger logger = LoggerFactory.getLogger(EcoTouchConnector.class);
59 * Create a network communication without having a current access token.
61 public EcoTouchConnector(String ip, String username, String password) {
63 this.username = username;
64 this.password = password;
69 * Create a network communication with access token. This speeds up
70 * retrieving values, because the log in step is omitted.
72 public EcoTouchConnector(String ip, String username, String password, List<String> cookies) {
74 this.username = username;
75 this.password = password;
76 this.cookies = cookies;
79 private synchronized void trylogin(boolean force) throws Exception {
80 if (!force && cookies != null) {
81 // we've a login token already
87 private void login() throws IOException {
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()));
100 line2 = in.readLine();
101 } catch (MalformedURLException e) {
102 cause = e.toString();
103 } catch (Exception e) {
104 cause = e.toString();
107 if (line2 != null && "#E_USER_DONT_EXIST".equals(line2.trim())) {
108 throw new IOException("Username does not exist.");
110 if (line2 != null && "#E_PASS_DONT_MATCH".equals(line2.trim())) {
111 throw new IOException("Password does not match.");
113 if (line2 != null && "#E_TOO_MANY_USERS".equals(line2.trim())) {
114 throw new IOException("Too many users already logged in.");
116 if (cookies == null) {
118 throw new IOException("Cannot login");
120 throw new IOException("Cannot login: " + cause);
125 public void logout() {
126 if (cookies != null) {
128 URL logouturl = new URL("http://" + ip + "/cgi/logout");
129 logouturl.openConnection();
130 } catch (Exception e) {
137 * Request a value from the heat pump
140 * The register to query (e.g. "A1")
141 * @return value This value is a 16-bit integer.
143 public String getValue(String tag) throws Exception {
144 Map<String, String> result = getValues(Set.of(tag));
145 String value = result.get(tag);
148 logger.debug("Cannot get value for tag '{}' from Waterkotte EcoTouch.", tag);
149 throw new EcoTouchException("invalid response from EcoTouch");
156 * Request multiple values from the heat pump
159 * The registers to query (e.g. "A1")
160 * @return values A map of tags and their respective string values
162 public Map<String, String> getValues(Set<String> tags) throws Exception {
163 final Integer maxNum = 100;
165 Map<String, String> result = new HashMap<String, String>();
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()));
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));
177 query = new StringBuilder();
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));
191 * Send a request to the heat pump and evaluate the result
194 * The URL to connect to
195 * @return values A map of tags and their respective string values
197 private Map<String, String> getValues(String url) throws Exception {
199 Map<String, String> result = new HashMap<String, String>();
200 int loginAttempt = 0;
201 while (loginAttempt < 2) {
202 BufferedReader reader = null;
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]);
211 InputStream response = connection.getInputStream();
212 reader = new BufferedReader(new InputStreamReader(response));
213 // the answer is s.th. like
218 while ((line = reader.readLine()) != null) {
219 String line2 = reader.readLine();
223 String doubleline = line + "\n" + line2;
224 Matcher m = responsePattern.matcher(doubleline);
226 String tag = m.group(1);
227 String value = m.group(3).trim();
228 result.put(tag, value);
232 if (result.isEmpty()) {
233 // s.th. went wrong; try to log in again
234 throw new EcoTouchException();
239 } catch (Exception e) {
240 if (loginAttempt == 0) {
246 if (reader != null) {
259 * The register to set (e.g. "A1")
261 * The 16-bit integer to set the register to
262 * @return value This value is a 16-bit integer.
264 public int setValue(String tag, int value) throws Exception {
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;
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]);
281 InputStream response = connection.getInputStream();
282 reader = new BufferedReader(new InputStreamReader(response));
283 body = new StringBuilder();
285 while ((line = reader.readLine()) != null) {
286 body.append(line + "\n");
288 if (body.toString().contains("#" + tag)) {
292 // s.th. went wrong; try to log in
293 throw new EcoTouchException();
294 } catch (Exception e) {
295 if (loginAttempt == 0) {
301 if (reader != null) {
307 if (body == null || !body.toString().contains("#" + tag)) {
309 logger.debug("Cannot get value for tag '{}' from Waterkotte EcoTouch.", tag);
310 throw new EcoTouchException("invalid response from EcoTouch");
313 // ok, the body now contains s.th. like
317 Matcher m = responsePattern.matcher(body.toString());
318 boolean b = m.find();
320 // ill formatted response
321 logger.debug("ill formatted response: '{}'", body);
322 throw new EcoTouchException("invalid response from EcoTouch");
325 logger.debug("response: '{}'", body.toString());
326 return Integer.parseInt(m.group(3));
330 * Authentication token. Store this and use it, when creating the next
331 * instance of this class.
333 * @return cookies: This includes the authentication token retrieved during
337 public List<String> getCookies() {