2 * Copyright (c) 2010-2021 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;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
33 * Network communication with Waterkotte EcoTouch heat pumps.
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).
39 * @author Sebastian Held <sebastian.held@gmx.de> - Initial contribution
44 public class EcoTouchConnector {
46 private String username;
47 private String password;
50 static Pattern responsePattern = Pattern.compile("#(.+)\\s+S_OK[^0-9-]+([0-9-]+)\\s+([0-9-.]+)");
52 private final Logger logger = LoggerFactory.getLogger(EcoTouchConnector.class);
55 * Create a network communication without having a current access token.
57 public EcoTouchConnector(String ip, String username, String password) {
59 this.username = username;
60 this.password = password;
65 * Create a network communication with access token. This speeds up
66 * retrieving values, because the log in step is omitted.
68 public EcoTouchConnector(String ip, String username, String password, List<String> cookies) {
70 this.username = username;
71 this.password = password;
72 this.cookies = cookies;
75 private synchronized void trylogin(boolean force) throws Exception {
76 if (!force && cookies != null) {
77 // we've a login token already
83 private void login() throws IOException {
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()));
96 line2 = in.readLine();
97 } catch (MalformedURLException e) {
99 } catch (Exception e) {
100 cause = e.toString();
103 if (line2 != null && line2.trim().equals("#E_USER_DONT_EXIST")) {
104 throw new IOException("Username does not exist.");
106 if (line2 != null && line2.trim().equals("#E_PASS_DONT_MATCH")) {
107 throw new IOException("Password does not match.");
109 if (line2 != null && line2.trim().equals("#E_TOO_MANY_USERS")) {
110 throw new IOException("Too many users already logged in.");
112 if (cookies == null) {
114 throw new IOException("Cannot login");
116 throw new IOException("Cannot login: " + cause);
120 public void logout() {
121 if (cookies != null) {
123 URL logouturl = new URL("http://" + ip + "/cgi/logout");
124 logouturl.openConnection();
125 } catch (Exception e) {
132 * Request a value from the heat pump
135 * The register to query (e.g. "A1")
136 * @return value This value is a 16-bit integer.
138 public String getValue(String tag) throws Exception {
139 Map<String, String> result = getValues(Set.of(tag));
140 String value = result.get(tag);
143 logger.debug("Cannot get value for tag '{}' from Waterkotte EcoTouch.", tag);
144 throw new EcoTouchException("invalid response from EcoTouch");
151 * Request multiple values from the heat pump
154 * The registers to query (e.g. "A1")
155 * @return values A map of tags and their respective string values
157 public Map<String, String> getValues(Set<String> tags) throws Exception {
158 final Integer maxNum = 100;
160 Map<String, String> result = new HashMap<String, String>();
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()));
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));
172 query = new StringBuilder();
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));
186 * Send a request to the heat pump and evaluate the result
189 * The URL to connect to
190 * @return values A map of tags and their respective string values
192 private Map<String, String> getValues(String url) throws Exception {
194 Map<String, String> result = new HashMap<String, String>();
195 int loginAttempt = 0;
196 while (loginAttempt < 2) {
197 BufferedReader reader = null;
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]);
206 InputStream response = connection.getInputStream();
207 reader = new BufferedReader(new InputStreamReader(response));
208 // the answer is s.th. like
213 while ((line = reader.readLine()) != null) {
214 String line2 = reader.readLine();
217 String doubleline = line + "\n" + line2;
218 Matcher m = responsePattern.matcher(doubleline);
220 String tag = m.group(1);
221 String value = m.group(3).trim();
222 result.put(tag, value);
226 if (result.isEmpty()) {
227 // s.th. went wrong; try to log in again
228 throw new EcoTouchException();
233 } catch (Exception e) {
234 if (loginAttempt == 0) {
252 * The register to set (e.g. "A1")
254 * The 16-bit integer to set the register to
255 * @return value This value is a 16-bit integer.
257 public int setValue(String tag, int value) throws Exception {
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;
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]);
274 InputStream response = connection.getInputStream();
275 reader = new BufferedReader(new InputStreamReader(response));
276 body = new StringBuilder();
278 while ((line = reader.readLine()) != null) {
279 body.append(line + "\n");
281 if (body.toString().contains("#" + tag)) {
285 // s.th. went wrong; try to log in
286 throw new EcoTouchException();
287 } catch (Exception e) {
288 if (loginAttempt == 0) {
299 if (body == null || !body.toString().contains("#" + tag)) {
301 logger.debug("Cannot get value for tag '{}' from Waterkotte EcoTouch.", tag);
302 throw new EcoTouchException("invalid response from EcoTouch");
305 // ok, the body now contains s.th. like
309 Matcher m = responsePattern.matcher(body.toString());
310 boolean b = m.find();
312 // ill formatted response
313 logger.debug("ill formatted response: '{}'", body);
314 throw new EcoTouchException("invalid response from EcoTouch");
317 logger.debug("response: '{}'", body.toString());
318 return Integer.parseInt(m.group(3));
322 * Authentication token. Store this and use it, when creating the next
323 * instance of this class.
325 * @return cookies: This includes the authentication token retrieved during
329 public List<String> getCookies() {