]> git.basschouten.com Git - openhab-addons.git/blob
e9a34a4f0bca2dada27eac6d7d8ad879ed738782
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.shelly.internal.api;
14
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLY_API_TIMEOUT_MS;
16 import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
17 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
18
19 import java.nio.charset.StandardCharsets;
20 import java.util.Base64;
21 import java.util.Map;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.eclipse.jetty.client.api.ContentResponse;
29 import org.eclipse.jetty.client.api.Request;
30 import org.eclipse.jetty.client.util.StringContentProvider;
31 import org.eclipse.jetty.http.HttpFields;
32 import org.eclipse.jetty.http.HttpHeader;
33 import org.eclipse.jetty.http.HttpMethod;
34 import org.eclipse.jetty.http.HttpStatus;
35 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
36 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
37 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 import com.google.gson.Gson;
42
43 /**
44  * {@link ShellyHttpClient} implements basic HTTP access
45  *
46  * @author Markus Michels - Initial contribution
47  */
48 @NonNullByDefault
49 public class ShellyHttpClient {
50     private final Logger logger = LoggerFactory.getLogger(ShellyHttpClient.class);
51
52     public static final String HTTP_HEADER_AUTH = "Authorization";
53     public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
54     public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8";
55     public static final String CONTENT_TYPE_FORM_URLENC = "application/x-www-form-urlencoded";
56
57     protected final HttpClient httpClient;
58     protected ShellyThingConfiguration config = new ShellyThingConfiguration();
59     protected String thingName;
60     protected final Gson gson = new Gson();
61     protected int timeoutErrors = 0;
62     protected int timeoutsRecovered = 0;
63     private ShellyDeviceProfile profile;
64
65     public ShellyHttpClient(String thingName, ShellyThingInterface thing) {
66         this(thingName, thing.getThingConfig(), thing.getHttpClient());
67         this.profile = thing.getProfile();
68     }
69
70     public ShellyHttpClient(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
71         profile = new ShellyDeviceProfile();
72         this.thingName = thingName;
73         setConfig(thingName, config);
74         this.httpClient = httpClient;
75     }
76
77     public void initialize() throws ShellyApiException {
78     }
79
80     public void setConfig(String thingName, ShellyThingConfiguration config) {
81         this.thingName = thingName;
82         this.config = config;
83     }
84
85     /**
86      * Submit GET request and return response, check for invalid responses
87      *
88      * @param uri: URI (e.g. "/settings")
89      */
90     public <T> T callApi(String uri, Class<T> classOfT) throws ShellyApiException {
91         String json = httpRequest(uri);
92         return fromJson(gson, json, classOfT);
93     }
94
95     public <T> T postApi(String uri, String data, Class<T> classOfT) throws ShellyApiException {
96         String json = httpPost(uri, data);
97         return fromJson(gson, json, classOfT);
98     }
99
100     protected String httpRequest(String uri) throws ShellyApiException {
101         ShellyApiResult apiResult = new ShellyApiResult();
102         int retries = 3;
103         boolean timeout = false;
104         while (retries > 0) {
105             try {
106                 apiResult = innerRequest(HttpMethod.GET, uri, "");
107                 if (timeout) {
108                     logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
109                             apiResult.getUrl());
110                     timeoutsRecovered++;
111                 }
112                 return apiResult.response; // successful
113             } catch (ShellyApiException e) {
114                 if (e.isConnectionError()
115                         || (!e.isTimeout() && !apiResult.isHttpServerError()) && !apiResult.isNotFound()
116                         || profile.hasBattery || (retries == 0)) {
117                     // Sensor in sleep mode or API exception for non-battery device or retry counter expired
118                     throw e; // non-timeout exception
119                 }
120
121                 timeout = true;
122                 retries--;
123                 timeoutErrors++; // count the retries
124                 logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
125             }
126         }
127         throw new ShellyApiException("API Timeout or inconsistent result"); // successful
128     }
129
130     public String httpPost(String uri, String data) throws ShellyApiException {
131         return innerRequest(HttpMethod.POST, uri, data).response;
132     }
133
134     private ShellyApiResult innerRequest(HttpMethod method, String uri, String data) throws ShellyApiException {
135         Request request = null;
136         String url = "http://" + config.deviceIp + uri;
137         ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
138
139         try {
140             request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
141                     TimeUnit.MILLISECONDS);
142
143             if (!config.password.isEmpty() && !getString(data).contains("\"auth\":{")) {
144                 String value = config.userId + ":" + config.password;
145                 request.header(HTTP_HEADER_AUTH,
146                         HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
147             }
148             fillPostData(request, data);
149             logger.trace("{}: HTTP {} for {} {}", thingName, method, url, data);
150
151             // Do request and get response
152             ContentResponse contentResponse = request.send();
153             apiResult = new ShellyApiResult(contentResponse);
154             apiResult.httpCode = contentResponse.getStatus();
155             String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
156             logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
157
158             if (response.contains("\"error\":{")) { // Gen2
159                 Shelly2RpcBaseMessage message = gson.fromJson(response, Shelly2RpcBaseMessage.class);
160                 if (message != null && message.error != null) {
161                     apiResult.httpCode = message.error.code;
162                     apiResult.response = message.error.message;
163                     if (getInteger(message.error.code) == HttpStatus.UNAUTHORIZED_401) {
164                         apiResult.authResponse = getString(message.error.message).replaceAll("\\\"", "\"");
165                     }
166                 }
167             }
168             HttpFields headers = contentResponse.getHeaders();
169             String auth = headers.get(HttpHeader.WWW_AUTHENTICATE);
170             if (!getString(auth).isEmpty()) {
171                 apiResult.authResponse = auth;
172             }
173
174             // validate response, API errors are reported as Json
175             if (apiResult.httpCode != HttpStatus.OK_200) {
176                 throw new ShellyApiException(apiResult);
177             }
178
179             if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/")
180                     && !url.contains("/sta_cache_reset")) {
181                 throw new ShellyApiException("Unexpected response: " + response);
182             }
183         } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
184             ShellyApiException ex = new ShellyApiException(apiResult, e);
185             if (!ex.isConnectionError() && !ex.isTimeout()) { // will be handled by the caller
186                 logger.trace("{}: API call returned exception", thingName, ex);
187             }
188             throw ex;
189         }
190         return apiResult;
191     }
192
193     /**
194      * Fill in POST data, set http headers
195      *
196      * @param request HTTP request structure
197      * @param data POST data, might be empty
198      */
199     private void fillPostData(Request request, String data) {
200         boolean json = data.startsWith("{") || data.contains("\": {");
201         String type = json ? CONTENT_TYPE_JSON : CONTENT_TYPE_FORM_URLENC;
202         request.header(HttpHeader.CONTENT_TYPE, type);
203         if (!data.isEmpty()) {
204             StringContentProvider postData;
205             postData = new StringContentProvider(type, data, StandardCharsets.UTF_8);
206             request.content(postData);
207             request.header(HttpHeader.CONTENT_LENGTH, Long.toString(postData.getLength()));
208         }
209     }
210
211     /**
212      * Format POST body depending on content type (JSON or form encoded)
213      *
214      * @param dataMap Field list
215      * @param json true=JSON format, false=form encoded
216      * @return formatted body
217      */
218     public static String buildPostData(Map<String, String> dataMap, boolean json) {
219         String data = "";
220         for (Map.Entry<String, String> e : dataMap.entrySet()) {
221             data = data + (data.isEmpty() ? "" : json ? ", " : "&");
222             if (!json) {
223                 data = data + e.getKey() + "=" + e.getValue();
224             } else {
225                 data = data + "\"" + e.getKey() + "\" : \"" + e.getValue() + "\"";
226             }
227         }
228         return json ? "{ " + data + " }" : data;
229     }
230
231     public String getControlUriPrefix(Integer id) {
232         String uri = "";
233         if (profile.isLight || profile.isDimmer) {
234             if (profile.isDuo || profile.isDimmer) {
235                 // Duo + Dimmer
236                 uri = SHELLY_URL_CONTROL_LIGHT;
237             } else {
238                 // Bulb + RGBW2
239                 uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
240             }
241         } else {
242             // Roller, Relay
243             uri = SHELLY_URL_CONTROL_RELEAY;
244         }
245         uri = uri + "/" + id;
246         return uri;
247     }
248
249     public int getTimeoutErrors() {
250         return timeoutErrors;
251     }
252
253     public int getTimeoutsRecovered() {
254         return timeoutsRecovered;
255     }
256 }