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