2 * Copyright (c) 2010-2022 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.concurrent.ExecutionException;
19 import java.util.concurrent.TimeUnit;
20 import java.util.concurrent.TimeoutException;
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.SpecialModeKind;
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;
43 * Handles performing the actual HTTP requests for communicating with Daikin air conditioning units.
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
52 public class DaikinWebTargets {
53 private static final int TIMEOUT_MS = 5000;
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;
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;
72 private @Nullable String uuid;
73 private final @Nullable HttpClient httpClient;
75 private Logger logger = LoggerFactory.getLogger(DaikinWebTargets.class);
77 public DaikinWebTargets(@Nullable HttpClient httpClient, @Nullable String host, @Nullable Boolean secure,
78 @Nullable String uuid) {
79 this.httpClient = httpClient;
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";
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";
102 // Standard Daikin API
103 public BasicInfo getBasicInfo() throws DaikinCommunicationException {
104 String response = invoke(getBasicInfoUri);
105 return BasicInfo.parse(response);
108 public ControlInfo getControlInfo() throws DaikinCommunicationException {
109 String response = invoke(getControlInfoUri);
110 return ControlInfo.parse(response);
113 public void setControlInfo(ControlInfo info) throws DaikinCommunicationException {
114 Map<String, String> queryParams = info.getParamString();
115 invoke(setControlInfoUri, queryParams);
118 public SensorInfo getSensorInfo() throws DaikinCommunicationException {
119 String response = invoke(getSensorInfoUri);
120 return SensorInfo.parse(response);
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);
130 public EnergyInfoYear getEnergyInfoYear() throws DaikinCommunicationException {
131 String response = invoke(getEnergyInfoYearUri);
132 return EnergyInfoYear.parse(response);
135 public EnergyInfoDayAndWeek getEnergyInfoDayAndWeek() throws DaikinCommunicationException {
136 String response = invoke(getEnergyInfoWeekUri);
137 return EnergyInfoDayAndWeek.parse(response);
140 public boolean setSpecialMode(SpecialModeKind specialModeKind, boolean state) throws DaikinCommunicationException {
141 Map<String, String> queryParams = new HashMap<>();
142 queryParams.put("spmode_kind", String.valueOf(specialModeKind.getValue()));
143 queryParams.put("set_spmode", state ? "1" : "0");
144 String response = invoke(setSpecialModeUri, queryParams);
145 return !response.contains("ret=OK");
148 // Daikin Airbase API
149 public AirbaseControlInfo getAirbaseControlInfo() throws DaikinCommunicationException {
150 String response = invoke(getAirbaseControlInfoUri);
151 return AirbaseControlInfo.parse(response);
154 public void setAirbaseControlInfo(AirbaseControlInfo info) throws DaikinCommunicationException {
155 Map<String, String> queryParams = info.getParamString();
156 invoke(setAirbaseControlInfoUri, queryParams);
159 public SensorInfo getAirbaseSensorInfo() throws DaikinCommunicationException {
160 String response = invoke(getAirbaseSensorInfoUri);
161 return SensorInfo.parse(response);
164 public AirbaseBasicInfo getAirbaseBasicInfo() throws DaikinCommunicationException {
165 String response = invoke(getAirbaseBasicInfoUri);
166 return AirbaseBasicInfo.parse(response);
169 public AirbaseModelInfo getAirbaseModelInfo() throws DaikinCommunicationException {
170 String response = invoke(getAirbaseModelInfoUri);
171 return AirbaseModelInfo.parse(response);
174 public AirbaseZoneInfo getAirbaseZoneInfo() throws DaikinCommunicationException {
175 String response = invoke(getAirbaseZoneInfoUri);
176 return AirbaseZoneInfo.parse(response);
179 public void setAirbaseZoneInfo(AirbaseZoneInfo zoneinfo) throws DaikinCommunicationException {
180 Map<String, String> queryParams = zoneinfo.getParamString();
181 invoke(setAirbaseZoneInfoUri, queryParams);
184 private String invoke(String uri) throws DaikinCommunicationException {
185 return invoke(uri, null);
188 private synchronized String invoke(String url, @Nullable Map<String, String> params)
189 throws DaikinCommunicationException {
190 int attemptCount = 1;
194 String result = executeUrl(url, params);
195 if (attemptCount > 1) {
196 logger.debug("HTTP request successful on attempt #{}: {}", attemptCount, url);
199 } catch (ExecutionException | TimeoutException e) {
200 if (attemptCount >= 3) {
201 logger.debug("HTTP request failed after {} attempts: {}", attemptCount, url, e);
202 Throwable rootCause = getRootCause(e);
203 String message = rootCause.getMessage();
204 // EOFException message is too verbose/gibberish
205 if (message == null || rootCause instanceof EOFException) {
206 message = "Connection error";
208 throw new DaikinCommunicationException(message);
210 logger.debug("HTTP request error on attempt #{}: {} {}", attemptCount, url, e.getMessage());
211 Thread.sleep(500 * attemptCount);
215 } catch (InterruptedException e) {
216 Thread.currentThread().interrupt();
217 throw new DaikinCommunicationException("Execution interrupted");
221 private String executeUrl(String url, @Nullable Map<String, String> params)
222 throws InterruptedException, TimeoutException, ExecutionException, DaikinCommunicationException {
223 Request request = httpClient.newRequest(url).method(HttpMethod.GET).timeout(TIMEOUT_MS, TimeUnit.MILLISECONDS);
225 request.header("X-Daikin-uuid", uuid);
226 logger.trace("Header: X-Daikin-uuid: {}", uuid);
228 if (params != null) {
229 params.forEach((key, value) -> request.param(key, value));
231 logger.trace("Calling url: {}", request.getURI());
233 ContentResponse response = request.send();
235 if (response.getStatus() != HttpStatus.OK_200) {
236 logger.debug("Daikin controller HTTP status: {} - {} {}", response.getStatus(), response.getReason(), url);
239 if (response.getStatus() == HttpStatus.FORBIDDEN_403) {
240 throw new DaikinCommunicationForbiddenException("Daikin controller access denied. Check uuid/key.");
243 return response.getContentAsString();
246 private Throwable getRootCause(Throwable exception) {
247 Throwable cause = exception.getCause();
248 while (cause != null) {
250 cause = cause.getCause();