2 * Copyright (c) 2010-2024 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.daikin.internal;
15 import java.io.EOFException;
16 import java.util.HashMap;
18 import java.util.Optional;
19 import java.util.concurrent.ExecutionException;
20 import java.util.concurrent.TimeUnit;
21 import java.util.concurrent.TimeoutException;
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;
46 * Handles performing the actual HTTP requests for communicating with Daikin air conditioning units.
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
55 public class DaikinWebTargets {
56 private static final int TIMEOUT_MS = 5000;
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;
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;
77 private @Nullable String uuid;
78 private final @Nullable HttpClient httpClient;
80 private Logger logger = LoggerFactory.getLogger(DaikinWebTargets.class);
82 public DaikinWebTargets(@Nullable HttpClient httpClient, @Nullable String host, @Nullable Boolean secure,
83 @Nullable String uuid) {
84 this.httpClient = httpClient;
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";
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";
109 // Standard Daikin API
110 public BasicInfo getBasicInfo() throws DaikinCommunicationException {
111 String response = invoke(getBasicInfoUri);
112 return BasicInfo.parse(response);
115 public ControlInfo getControlInfo() throws DaikinCommunicationException {
116 String response = invoke(getControlInfoUri);
117 return ControlInfo.parse(response);
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");
127 public SensorInfo getSensorInfo() throws DaikinCommunicationException {
128 String response = invoke(getSensorInfoUri);
129 return SensorInfo.parse(response);
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);
139 public EnergyInfoYear getEnergyInfoYear() throws DaikinCommunicationException {
140 String response = invoke(getEnergyInfoYearUri);
141 return EnergyInfoYear.parse(response);
144 public EnergyInfoDayAndWeek getEnergyInfoDayAndWeek() throws DaikinCommunicationException {
145 String response = invoke(getEnergyInfoWeekUri);
146 return EnergyInfoDayAndWeek.parse(response);
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");
154 ControlInfo controlInfo = getControlInfo();
155 if (!controlInfo.advancedMode.isUndefined()) {
156 queryParams.put("spmode_kind", controlInfo.getSpecialMode().getValue());
159 queryParams.put("set_spmode", "1");
160 queryParams.put("spmode_kind", newMode.getValue());
162 String response = invoke(setSpecialModeUri, queryParams);
163 if (!response.contains("ret=OK")) {
164 logger.warn("Error setting special mode. Response: '{}'", response);
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);
177 public DemandControl getDemandControl() throws DaikinCommunicationException {
178 String response = invoke(getDemandControlUri);
179 return DemandControl.parse(response);
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");
189 // Daikin Airbase API
190 public AirbaseControlInfo getAirbaseControlInfo() throws DaikinCommunicationException {
191 String response = invoke(getAirbaseControlInfoUri);
192 return AirbaseControlInfo.parse(response);
195 public void setAirbaseControlInfo(AirbaseControlInfo info) throws DaikinCommunicationException {
196 Map<String, String> queryParams = info.getParamString();
197 invoke(setAirbaseControlInfoUri, queryParams);
200 public SensorInfo getAirbaseSensorInfo() throws DaikinCommunicationException {
201 String response = invoke(getAirbaseSensorInfoUri);
202 return SensorInfo.parse(response);
205 public AirbaseBasicInfo getAirbaseBasicInfo() throws DaikinCommunicationException {
206 String response = invoke(getAirbaseBasicInfoUri);
207 return AirbaseBasicInfo.parse(response);
210 public AirbaseModelInfo getAirbaseModelInfo() throws DaikinCommunicationException {
211 String response = invoke(getAirbaseModelInfoUri);
212 return AirbaseModelInfo.parse(response);
215 public AirbaseZoneInfo getAirbaseZoneInfo() throws DaikinCommunicationException {
216 String response = invoke(getAirbaseZoneInfoUri);
217 return AirbaseZoneInfo.parse(response);
220 public void setAirbaseZoneInfo(AirbaseZoneInfo zoneinfo) throws DaikinCommunicationException {
221 Map<String, String> queryParams = zoneinfo.getParamString();
222 invoke(setAirbaseZoneInfoUri, queryParams);
225 private String invoke(String uri) throws DaikinCommunicationException {
226 return invoke(uri, null);
229 private synchronized String invoke(String url, @Nullable Map<String, String> params)
230 throws DaikinCommunicationException {
231 int attemptCount = 1;
235 String result = executeUrl(url, params);
236 if (attemptCount > 1) {
237 logger.debug("HTTP request successful on attempt #{}: {}", attemptCount, url);
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";
249 throw new DaikinCommunicationException(message);
251 logger.debug("HTTP request error on attempt #{}: {} {}", attemptCount, url, e.getMessage());
252 Thread.sleep(500 * attemptCount);
256 } catch (InterruptedException e) {
257 Thread.currentThread().interrupt();
258 throw new DaikinCommunicationException("Execution interrupted");
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);
266 request.header("X-Daikin-uuid", uuid);
267 logger.trace("Header: X-Daikin-uuid: {}", uuid);
269 if (params != null) {
270 params.forEach((key, value) -> request.param(key, value));
272 logger.trace("Calling url: {}", request.getURI());
274 ContentResponse response = request.send();
276 if (response.getStatus() != HttpStatus.OK_200) {
277 logger.debug("Daikin controller HTTP status: {} - {} {}", response.getStatus(), response.getReason(), url);
280 if (response.getStatus() == HttpStatus.FORBIDDEN_403) {
281 throw new DaikinCommunicationForbiddenException("Daikin controller access denied. Check uuid/key.");
284 return response.getContentAsString();
287 private Throwable getRootCause(Throwable exception) {
288 Throwable cause = exception.getCause();
289 while (cause != null) {
291 cause = cause.getCause();