]> git.basschouten.com Git - openhab-addons.git/blob
af0e2c9cd19e8ca91bc6ea9c4d9f7b07bd8de7b4
[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.daikin.internal;
14
15 import java.io.EOFException;
16 import java.util.HashMap;
17 import java.util.Map;
18 import java.util.concurrent.ExecutionException;
19 import java.util.concurrent.TimeUnit;
20 import java.util.concurrent.TimeoutException;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.eclipse.jetty.client.HttpClient;
25 import org.eclipse.jetty.client.api.ContentResponse;
26 import org.eclipse.jetty.client.api.Request;
27 import org.eclipse.jetty.http.HttpMethod;
28 import org.eclipse.jetty.http.HttpStatus;
29 import org.openhab.binding.daikin.internal.api.BasicInfo;
30 import org.openhab.binding.daikin.internal.api.ControlInfo;
31 import org.openhab.binding.daikin.internal.api.EnergyInfoDayAndWeek;
32 import org.openhab.binding.daikin.internal.api.EnergyInfoYear;
33 import org.openhab.binding.daikin.internal.api.Enums.SpecialMode;
34 import org.openhab.binding.daikin.internal.api.SensorInfo;
35 import org.openhab.binding.daikin.internal.api.airbase.AirbaseBasicInfo;
36 import org.openhab.binding.daikin.internal.api.airbase.AirbaseControlInfo;
37 import org.openhab.binding.daikin.internal.api.airbase.AirbaseModelInfo;
38 import org.openhab.binding.daikin.internal.api.airbase.AirbaseZoneInfo;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  * Handles performing the actual HTTP requests for communicating with Daikin air conditioning units.
44  *
45  * @author Tim Waterhouse - Initial Contribution
46  * @author Paul Smedley <paul@smedley.id.au> - Modifications to support Airbase Controllers
47  * @author Jimmy Tanagra - Add support for https and Daikin's uuid authentication
48  *         Implement connection retry
49  *
50  */
51 @NonNullByDefault
52 public class DaikinWebTargets {
53     private static final int TIMEOUT_MS = 5000;
54
55     private String getBasicInfoUri;
56     private String setControlInfoUri;
57     private String getControlInfoUri;
58     private String getSensorInfoUri;
59     private String registerUuidUri;
60     private String getEnergyInfoYearUri;
61     private String getEnergyInfoWeekUri;
62     private String setSpecialModeUri;
63
64     private String setAirbaseControlInfoUri;
65     private String getAirbaseControlInfoUri;
66     private String getAirbaseSensorInfoUri;
67     private String getAirbaseBasicInfoUri;
68     private String getAirbaseModelInfoUri;
69     private String getAirbaseZoneInfoUri;
70     private String setAirbaseZoneInfoUri;
71
72     private @Nullable String uuid;
73     private final @Nullable HttpClient httpClient;
74
75     private Logger logger = LoggerFactory.getLogger(DaikinWebTargets.class);
76
77     public DaikinWebTargets(@Nullable HttpClient httpClient, @Nullable String host, @Nullable Boolean secure,
78             @Nullable String uuid) {
79         this.httpClient = httpClient;
80         this.uuid = uuid;
81
82         String baseUri = (secure != null && secure.booleanValue() ? "https://" : "http://") + host + "/";
83         getBasicInfoUri = baseUri + "common/basic_info";
84         setControlInfoUri = baseUri + "aircon/set_control_info";
85         getControlInfoUri = baseUri + "aircon/get_control_info";
86         getSensorInfoUri = baseUri + "aircon/get_sensor_info";
87         registerUuidUri = baseUri + "common/register_terminal";
88         getEnergyInfoYearUri = baseUri + "aircon/get_year_power_ex";
89         getEnergyInfoWeekUri = baseUri + "aircon/get_week_power_ex";
90         setSpecialModeUri = baseUri + "aircon/set_special_mode";
91
92         // Daikin Airbase API
93         getAirbaseBasicInfoUri = baseUri + "skyfi/common/basic_info";
94         setAirbaseControlInfoUri = baseUri + "skyfi/aircon/set_control_info";
95         getAirbaseControlInfoUri = baseUri + "skyfi/aircon/get_control_info";
96         getAirbaseSensorInfoUri = baseUri + "skyfi/aircon/get_sensor_info";
97         getAirbaseModelInfoUri = baseUri + "skyfi/aircon/get_model_info";
98         getAirbaseZoneInfoUri = baseUri + "skyfi/aircon/get_zone_setting";
99         setAirbaseZoneInfoUri = baseUri + "skyfi/aircon/set_zone_setting";
100     }
101
102     // Standard Daikin API
103     public BasicInfo getBasicInfo() throws DaikinCommunicationException {
104         String response = invoke(getBasicInfoUri);
105         return BasicInfo.parse(response);
106     }
107
108     public ControlInfo getControlInfo() throws DaikinCommunicationException {
109         String response = invoke(getControlInfoUri);
110         return ControlInfo.parse(response);
111     }
112
113     public void setControlInfo(ControlInfo info) throws DaikinCommunicationException {
114         Map<String, String> queryParams = info.getParamString();
115         invoke(setControlInfoUri, queryParams);
116     }
117
118     public SensorInfo getSensorInfo() throws DaikinCommunicationException {
119         String response = invoke(getSensorInfoUri);
120         return SensorInfo.parse(response);
121     }
122
123     public void registerUuid(String key) throws DaikinCommunicationException {
124         Map<String, String> params = new HashMap<>();
125         params.put("key", key);
126         String response = invoke(registerUuidUri, params);
127         logger.debug("registerUuid result: {}", response);
128     }
129
130     public EnergyInfoYear getEnergyInfoYear() throws DaikinCommunicationException {
131         String response = invoke(getEnergyInfoYearUri);
132         return EnergyInfoYear.parse(response);
133     }
134
135     public EnergyInfoDayAndWeek getEnergyInfoDayAndWeek() throws DaikinCommunicationException {
136         String response = invoke(getEnergyInfoWeekUri);
137         return EnergyInfoDayAndWeek.parse(response);
138     }
139
140     public void setSpecialMode(SpecialMode newMode) throws DaikinCommunicationException {
141         Map<String, String> queryParams = new HashMap<>();
142         if (newMode == SpecialMode.NORMAL) {
143             queryParams.put("set_spmode", "0");
144
145             ControlInfo controlInfo = getControlInfo();
146             if (!controlInfo.advancedMode.isUndefined()) {
147                 queryParams.put("spmode_kind", controlInfo.getSpecialMode().getValue());
148             }
149         } else {
150             queryParams.put("set_spmode", "1");
151             queryParams.put("spmode_kind", newMode.getValue());
152         }
153         String response = invoke(setSpecialModeUri, queryParams);
154         if (!response.contains("ret=OK")) {
155             logger.warn("Error setting special mode. Response: '{}'", response);
156         }
157     }
158
159     public void setStreamerMode(boolean state) throws DaikinCommunicationException {
160         Map<String, String> queryParams = new HashMap<>();
161         queryParams.put("en_streamer", state ? "1" : "0");
162         String response = invoke(setSpecialModeUri, queryParams);
163         if (!response.contains("ret=OK")) {
164             logger.warn("Error setting streamer mode. Response: '{}'", response);
165         }
166     }
167
168     // Daikin Airbase API
169     public AirbaseControlInfo getAirbaseControlInfo() throws DaikinCommunicationException {
170         String response = invoke(getAirbaseControlInfoUri);
171         return AirbaseControlInfo.parse(response);
172     }
173
174     public void setAirbaseControlInfo(AirbaseControlInfo info) throws DaikinCommunicationException {
175         Map<String, String> queryParams = info.getParamString();
176         invoke(setAirbaseControlInfoUri, queryParams);
177     }
178
179     public SensorInfo getAirbaseSensorInfo() throws DaikinCommunicationException {
180         String response = invoke(getAirbaseSensorInfoUri);
181         return SensorInfo.parse(response);
182     }
183
184     public AirbaseBasicInfo getAirbaseBasicInfo() throws DaikinCommunicationException {
185         String response = invoke(getAirbaseBasicInfoUri);
186         return AirbaseBasicInfo.parse(response);
187     }
188
189     public AirbaseModelInfo getAirbaseModelInfo() throws DaikinCommunicationException {
190         String response = invoke(getAirbaseModelInfoUri);
191         return AirbaseModelInfo.parse(response);
192     }
193
194     public AirbaseZoneInfo getAirbaseZoneInfo() throws DaikinCommunicationException {
195         String response = invoke(getAirbaseZoneInfoUri);
196         return AirbaseZoneInfo.parse(response);
197     }
198
199     public void setAirbaseZoneInfo(AirbaseZoneInfo zoneinfo) throws DaikinCommunicationException {
200         Map<String, String> queryParams = zoneinfo.getParamString();
201         invoke(setAirbaseZoneInfoUri, queryParams);
202     }
203
204     private String invoke(String uri) throws DaikinCommunicationException {
205         return invoke(uri, null);
206     }
207
208     private synchronized String invoke(String url, @Nullable Map<String, String> params)
209             throws DaikinCommunicationException {
210         int attemptCount = 1;
211         try {
212             while (true) {
213                 try {
214                     String result = executeUrl(url, params);
215                     if (attemptCount > 1) {
216                         logger.debug("HTTP request successful on attempt #{}: {}", attemptCount, url);
217                     }
218                     return result;
219                 } catch (ExecutionException | TimeoutException e) {
220                     if (attemptCount >= 3) {
221                         logger.debug("HTTP request failed after {} attempts: {}", attemptCount, url, e);
222                         Throwable rootCause = getRootCause(e);
223                         String message = rootCause.getMessage();
224                         // EOFException message is too verbose/gibberish
225                         if (message == null || rootCause instanceof EOFException) {
226                             message = "Connection error";
227                         }
228                         throw new DaikinCommunicationException(message);
229                     }
230                     logger.debug("HTTP request error on attempt #{}: {} {}", attemptCount, url, e.getMessage());
231                     Thread.sleep(500 * attemptCount);
232                     attemptCount++;
233                 }
234             }
235         } catch (InterruptedException e) {
236             Thread.currentThread().interrupt();
237             throw new DaikinCommunicationException("Execution interrupted");
238         }
239     }
240
241     private String executeUrl(String url, @Nullable Map<String, String> params)
242             throws InterruptedException, TimeoutException, ExecutionException, DaikinCommunicationException {
243         Request request = httpClient.newRequest(url).method(HttpMethod.GET).timeout(TIMEOUT_MS, TimeUnit.MILLISECONDS);
244         if (uuid != null) {
245             request.header("X-Daikin-uuid", uuid);
246             logger.trace("Header: X-Daikin-uuid: {}", uuid);
247         }
248         if (params != null) {
249             params.forEach((key, value) -> request.param(key, value));
250         }
251         logger.trace("Calling url: {}", request.getURI());
252
253         ContentResponse response = request.send();
254
255         if (response.getStatus() != HttpStatus.OK_200) {
256             logger.debug("Daikin controller HTTP status: {} - {} {}", response.getStatus(), response.getReason(), url);
257         }
258
259         if (response.getStatus() == HttpStatus.FORBIDDEN_403) {
260             throw new DaikinCommunicationForbiddenException("Daikin controller access denied. Check uuid/key.");
261         }
262
263         return response.getContentAsString();
264     }
265
266     private Throwable getRootCause(Throwable exception) {
267         Throwable cause = exception.getCause();
268         while (cause != null) {
269             exception = cause;
270             cause = cause.getCause();
271         }
272         return exception;
273     }
274 }