2 * Copyright (c) 2010-2023 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.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;
45 * Handles performing the actual HTTP requests for communicating with Daikin air conditioning units.
47 * @author Tim Waterhouse - Initial Contribution
48 * @author Paul Smedley <paul@smedley.id.au> - Modifications to support Airbase Controllers
49 * @author Jimmy Tanagra - Add support for https and Daikin's uuid authentication
50 * Implement connection retry
54 public class DaikinWebTargets {
55 private static final int TIMEOUT_MS = 5000;
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;
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;
74 private @Nullable String uuid;
75 private final @Nullable HttpClient httpClient;
77 private Logger logger = LoggerFactory.getLogger(DaikinWebTargets.class);
79 public DaikinWebTargets(@Nullable HttpClient httpClient, @Nullable String host, @Nullable Boolean secure,
80 @Nullable String uuid) {
81 this.httpClient = httpClient;
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";
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";
104 // Standard Daikin API
105 public BasicInfo getBasicInfo() throws DaikinCommunicationException {
106 String response = invoke(getBasicInfoUri);
107 return BasicInfo.parse(response);
110 public ControlInfo getControlInfo() throws DaikinCommunicationException {
111 String response = invoke(getControlInfoUri);
112 return ControlInfo.parse(response);
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");
122 public SensorInfo getSensorInfo() throws DaikinCommunicationException {
123 String response = invoke(getSensorInfoUri);
124 return SensorInfo.parse(response);
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);
134 public EnergyInfoYear getEnergyInfoYear() throws DaikinCommunicationException {
135 String response = invoke(getEnergyInfoYearUri);
136 return EnergyInfoYear.parse(response);
139 public EnergyInfoDayAndWeek getEnergyInfoDayAndWeek() throws DaikinCommunicationException {
140 String response = invoke(getEnergyInfoWeekUri);
141 return EnergyInfoDayAndWeek.parse(response);
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");
149 ControlInfo controlInfo = getControlInfo();
150 if (!controlInfo.advancedMode.isUndefined()) {
151 queryParams.put("spmode_kind", controlInfo.getSpecialMode().getValue());
154 queryParams.put("set_spmode", "1");
155 queryParams.put("spmode_kind", newMode.getValue());
157 String response = invoke(setSpecialModeUri, queryParams);
158 if (!response.contains("ret=OK")) {
159 logger.warn("Error setting special mode. Response: '{}'", response);
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);
172 // Daikin Airbase API
173 public AirbaseControlInfo getAirbaseControlInfo() throws DaikinCommunicationException {
174 String response = invoke(getAirbaseControlInfoUri);
175 return AirbaseControlInfo.parse(response);
178 public void setAirbaseControlInfo(AirbaseControlInfo info) throws DaikinCommunicationException {
179 Map<String, String> queryParams = info.getParamString();
180 invoke(setAirbaseControlInfoUri, queryParams);
183 public SensorInfo getAirbaseSensorInfo() throws DaikinCommunicationException {
184 String response = invoke(getAirbaseSensorInfoUri);
185 return SensorInfo.parse(response);
188 public AirbaseBasicInfo getAirbaseBasicInfo() throws DaikinCommunicationException {
189 String response = invoke(getAirbaseBasicInfoUri);
190 return AirbaseBasicInfo.parse(response);
193 public AirbaseModelInfo getAirbaseModelInfo() throws DaikinCommunicationException {
194 String response = invoke(getAirbaseModelInfoUri);
195 return AirbaseModelInfo.parse(response);
198 public AirbaseZoneInfo getAirbaseZoneInfo() throws DaikinCommunicationException {
199 String response = invoke(getAirbaseZoneInfoUri);
200 return AirbaseZoneInfo.parse(response);
203 public void setAirbaseZoneInfo(AirbaseZoneInfo zoneinfo) throws DaikinCommunicationException {
204 Map<String, String> queryParams = zoneinfo.getParamString();
205 invoke(setAirbaseZoneInfoUri, queryParams);
208 private String invoke(String uri) throws DaikinCommunicationException {
209 return invoke(uri, null);
212 private synchronized String invoke(String url, @Nullable Map<String, String> params)
213 throws DaikinCommunicationException {
214 int attemptCount = 1;
218 String result = executeUrl(url, params);
219 if (attemptCount > 1) {
220 logger.debug("HTTP request successful on attempt #{}: {}", attemptCount, url);
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";
232 throw new DaikinCommunicationException(message);
234 logger.debug("HTTP request error on attempt #{}: {} {}", attemptCount, url, e.getMessage());
235 Thread.sleep(500 * attemptCount);
239 } catch (InterruptedException e) {
240 Thread.currentThread().interrupt();
241 throw new DaikinCommunicationException("Execution interrupted");
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);
249 request.header("X-Daikin-uuid", uuid);
250 logger.trace("Header: X-Daikin-uuid: {}", uuid);
252 if (params != null) {
253 params.forEach((key, value) -> request.param(key, value));
255 logger.trace("Calling url: {}", request.getURI());
257 ContentResponse response = request.send();
259 if (response.getStatus() != HttpStatus.OK_200) {
260 logger.debug("Daikin controller HTTP status: {} - {} {}", response.getStatus(), response.getReason(), url);
263 if (response.getStatus() == HttpStatus.FORBIDDEN_403) {
264 throw new DaikinCommunicationForbiddenException("Daikin controller access denied. Check uuid/key.");
267 return response.getContentAsString();
270 private Throwable getRootCause(Throwable exception) {
271 Throwable cause = exception.getCause();
272 while (cause != null) {
274 cause = cause.getCause();