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