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.shelly.internal.api;
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.*;
19 import java.nio.charset.StandardCharsets;
20 import java.util.Base64;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
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;
41 import com.google.gson.Gson;
44 * {@link ShellyHttpClient} implements basic HTTP access
46 * @author Markus Michels - Initial contribution
49 public class ShellyHttpClient {
50 private final Logger logger = LoggerFactory.getLogger(ShellyHttpClient.class);
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";
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;
65 public ShellyHttpClient(String thingName, ShellyThingInterface thing) {
66 this(thingName, thing.getThingConfig(), thing.getHttpClient());
67 this.profile = thing.getProfile();
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;
77 public void initialize() throws ShellyApiException {
80 public void setConfig(String thingName, ShellyThingConfiguration config) {
81 this.thingName = thingName;
86 * Submit GET request and return response, check for invalid responses
88 * @param uri: URI (e.g. "/settings")
90 public <T> T callApi(String uri, Class<T> classOfT) throws ShellyApiException {
91 String json = httpRequest(uri);
92 return fromJson(gson, json, classOfT);
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);
100 protected String httpRequest(String uri) throws ShellyApiException {
101 ShellyApiResult apiResult = new ShellyApiResult();
103 boolean timeout = false;
104 while (retries > 0) {
106 apiResult = innerRequest(HttpMethod.GET, uri, "");
108 logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
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
123 timeoutErrors++; // count the retries
124 logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
127 throw new ShellyApiException("API Timeout or inconsistent result"); // successful
130 public String httpPost(String uri, String data) throws ShellyApiException {
131 return innerRequest(HttpMethod.POST, uri, data).response;
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);
140 request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
141 TimeUnit.MILLISECONDS);
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()));
148 fillPostData(request, data);
149 logger.trace("{}: HTTP {} for {} {}\n{}", thingName, method, url, data, request.getHeaders());
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 {}: {}\n{}", thingName, contentResponse.getStatus(), response,
157 contentResponse.getHeaders());
159 if (response.contains("\"error\":{")) { // Gen2
160 Shelly2RpcBaseMessage message = gson.fromJson(response, Shelly2RpcBaseMessage.class);
161 if (message != null && message.error != null) {
162 apiResult.httpCode = message.error.code;
163 apiResult.response = message.error.message;
164 if (getInteger(message.error.code) == HttpStatus.UNAUTHORIZED_401) {
165 apiResult.authResponse = getString(message.error.message).replaceAll("\\\"", "\"");
169 HttpFields headers = contentResponse.getHeaders();
170 String auth = headers.get(HttpHeader.WWW_AUTHENTICATE);
171 if (!getString(auth).isEmpty()) {
172 apiResult.authResponse = auth;
175 // validate response, API errors are reported as Json
176 if (apiResult.httpCode != HttpStatus.OK_200) {
177 throw new ShellyApiException(apiResult);
180 if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/")
181 && !url.contains("/sta_cache_reset")) {
182 throw new ShellyApiException("Unexpected response: " + response);
184 } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
185 ShellyApiException ex = new ShellyApiException(apiResult, e);
186 if (!ex.isConnectionError() && !ex.isTimeout()) { // will be handled by the caller
187 logger.trace("{}: API call returned exception", thingName, ex);
195 * Fill in POST data, set http headers
197 * @param request HTTP request structure
198 * @param data POST data, might be empty
200 private void fillPostData(Request request, String data) {
201 boolean json = data.startsWith("{") || data.contains("\": {");
202 String type = json ? CONTENT_TYPE_JSON : CONTENT_TYPE_FORM_URLENC;
203 request.header(HttpHeader.CONTENT_TYPE, type);
204 if (!data.isEmpty()) {
205 StringContentProvider postData;
206 postData = new StringContentProvider(type, data, StandardCharsets.UTF_8);
207 request.content(postData);
208 // request.header(HttpHeader.CONTENT_LENGTH, Long.toString(postData.getLength()));
213 * Format POST body depending on content type (JSON or form encoded)
215 * @param dataMap Field list
216 * @param json true=JSON format, false=form encoded
217 * @return formatted body
219 public static String buildPostData(Map<String, String> dataMap, boolean json) {
221 for (Map.Entry<String, String> e : dataMap.entrySet()) {
222 data = data + (data.isEmpty() ? "" : json ? ", " : "&");
224 data = data + e.getKey() + "=" + e.getValue();
226 data = data + "\"" + e.getKey() + "\" : \"" + e.getValue() + "\"";
229 return json ? "{ " + data + " }" : data;
232 public String getControlUriPrefix(Integer id) {
234 if (profile.isLight || profile.isDimmer) {
235 if (profile.isDuo || profile.isDimmer) {
237 uri = SHELLY_URL_CONTROL_LIGHT;
240 uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
244 uri = SHELLY_URL_CONTROL_RELEAY;
246 uri = uri + "/" + id;
250 public int getTimeoutErrors() {
251 return timeoutErrors;
254 public int getTimeoutsRecovered() {
255 return timeoutsRecovered;
258 public void postEvent(String device, String index, String event, Map<String, String> parms)
259 throws ShellyApiException {