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.bondhome.internal.api;
15 import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
17 import java.io.ByteArrayInputStream;
18 import java.io.InputStream;
19 import java.io.UnsupportedEncodingException;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayList;
22 import java.util.List;
24 import java.util.Objects;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.api.ContentResponse;
34 import org.eclipse.jetty.client.api.Request;
35 import org.eclipse.jetty.client.util.InputStreamContentProvider;
36 import org.eclipse.jetty.http.HttpMethod;
37 import org.eclipse.jetty.http.HttpStatus;
38 import org.openhab.binding.bondhome.internal.BondException;
39 import org.openhab.binding.bondhome.internal.handler.BondBridgeHandler;
40 import org.openhab.core.io.net.http.HttpClientFactory;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
45 import com.google.gson.Gson;
46 import com.google.gson.JsonElement;
47 import com.google.gson.JsonObject;
48 import com.google.gson.JsonParseException;
49 import com.google.gson.JsonParser;
52 * {@link BondHttpApi} wraps the Bond REST API and provides various low
53 * level function to access the device api (not cloud api).
55 * @author Sara Geleskie Damiano - Initial contribution
58 public class BondHttpApi {
59 private final Logger logger = LoggerFactory.getLogger(BondHttpApi.class);
60 private final BondBridgeHandler bridgeHandler;
61 private final HttpClientFactory httpClientFactory;
62 private Gson gson = new Gson();
64 public BondHttpApi(BondBridgeHandler bridgeHandler, final HttpClientFactory httpClientFactory) {
65 this.bridgeHandler = bridgeHandler;
66 this.httpClientFactory = httpClientFactory;
70 * Gets version information about the Bond bridge
72 * @return the {@link BondSysVersion}
73 * @throws BondException
75 public BondSysVersion getBridgeVersion() throws BondException {
76 String json = request("/v2/sys/version");
77 logger.trace("BondHome device info : {}", json);
79 return Objects.requireNonNull(gson.fromJson(json, BondSysVersion.class));
80 } catch (JsonParseException e) {
81 logger.debug("Could not parse sys/version JSON '{}'", json, e);
82 throw new BondException("@text/offline.comm-error.unparseable-response");
87 * Gets a list of the attached devices
89 * @return an array of device id's
90 * @throws BondException
92 public List<String> getDevices() throws BondException {
93 List<String> list = new ArrayList<>();
94 String json = request("/v2/devices/");
96 JsonElement element = JsonParser.parseString(json);
97 JsonObject obj = element.getAsJsonObject();
98 Set<Map.Entry<String, JsonElement>> entries = obj.entrySet();
99 for (Map.Entry<String, JsonElement> entry : entries) {
100 String key = entry.getKey();
101 if (!key.startsWith("_")) {
106 } catch (JsonParseException e) {
107 logger.debug("Could not parse devices JSON '{}'", json, e);
108 throw new BondException("@text/offline.comm-error.unparseable-response");
113 * Gets basic device information
115 * @param deviceId The ID of the device
116 * @return the {@link BondDevice}
117 * @throws BondException
119 public BondDevice getDevice(String deviceId) throws BondException {
120 String json = request("/v2/devices/" + deviceId);
121 logger.trace("BondHome device info : {}", json);
123 return Objects.requireNonNull(gson.fromJson(json, BondDevice.class));
124 } catch (JsonParseException e) {
125 logger.debug("Could not parse device {}'s JSON '{}'", deviceId, json, e);
126 throw new BondException("@text/offline.comm-error.unparseable-response");
131 * Gets the current state of a device
133 * @param deviceId The ID of the device
134 * @return the {@link BondDeviceState}
135 * @throws BondException
137 public BondDeviceState getDeviceState(String deviceId) throws BondException {
138 String json = request("/v2/devices/" + deviceId + "/state");
139 logger.trace("BondHome device state : {}", json);
141 return Objects.requireNonNull(gson.fromJson(json, BondDeviceState.class));
142 } catch (JsonParseException e) {
143 logger.debug("Could not parse device {}'s state JSON '{}'", deviceId, json, e);
144 throw new BondException("@text/offline.comm-error.unparseable-response");
149 * Gets the current properties of a device
151 * @param deviceId The ID of the device
152 * @return the {@link BondDeviceProperties}
153 * @throws BondException
155 public BondDeviceProperties getDeviceProperties(String deviceId) throws BondException {
156 String json = request("/v2/devices/" + deviceId + "/properties");
157 logger.trace("BondHome device properties : {}", json);
159 return Objects.requireNonNull(gson.fromJson(json, BondDeviceProperties.class));
160 } catch (JsonParseException e) {
161 logger.debug("Could not parse device {}'s property JSON '{}'", deviceId, json, e);
162 throw new BondException("@text/offline.comm-error.unparseable-response");
167 * Executes a device action
169 * @param deviceId The ID of the device
170 * @param actionId The Bond action
171 * @param argument An additional argument for the actions (such as the fan speed)
173 public synchronized void executeDeviceAction(String deviceId, BondDeviceAction action, @Nullable Integer argument) {
174 String url = "http://" + bridgeHandler.getBridgeIpAddress() + "/v2/devices/" + deviceId + "/actions/"
175 + action.getActionId();
176 String payload = "{}";
177 if (argument != null) {
178 payload = "{\"argument\":" + argument + "}";
180 InputStream content = new ByteArrayInputStream(payload.getBytes());
181 logger.debug("HTTP PUT to {} with content {}", url, payload);
183 final HttpClient httpClient = httpClientFactory.getCommonHttpClient();
184 final Request request = httpClient.newRequest(url).method(HttpMethod.PUT)
185 .header("BOND-Token", bridgeHandler.getBridgeToken())
186 .timeout(BOND_API_TIMEOUT_MS, TimeUnit.MILLISECONDS);
188 try (final InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(content)) {
189 request.content(inputStreamContentProvider, "application/json");
191 ContentResponse response;
193 response = request.send();
194 } catch (Exception e) {
195 logger.warn("Unable to execute device action {} against device {}: {}", deviceId, action, e.getMessage());
199 logger.debug("HTTP response from {}: {}", deviceId, response.getStatus());
203 * Submit GET request and return response, check for invalid responses
205 * @param uri: URI (e.g. "/settings")
207 private synchronized String request(String uri) throws BondException {
208 String httpResponse = "ERROR";
209 String url = "http://" + bridgeHandler.getBridgeIpAddress() + uri;
210 int numRetriesRemaining = 3;
213 logger.debug("HTTP GET to {}", url);
215 final HttpClient httpClient = httpClientFactory.getCommonHttpClient();
216 final Request request = httpClient.newRequest(url).method(HttpMethod.GET).header("BOND-Token",
217 bridgeHandler.getBridgeToken());
218 ContentResponse response;
219 response = request.send();
220 String encoding = response.getEncoding() != null ? response.getEncoding().replaceAll("\"", "").trim()
221 : StandardCharsets.UTF_8.name();
223 httpResponse = new String(response.getContent(), encoding);
224 } catch (UnsupportedEncodingException e) {
225 throw new BondException("@text/offline.comm-error.no-response");
227 // handle known errors
228 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
229 // Don't retry or throw an exception if we get unauthorized, just set the bridge offline
230 bridgeHandler.setBridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR,
231 "@text/offline.conf-error.incorrect-local-token");
232 throw new BondException("@text/offline.conf-error.incorrect-local-token", true);
234 if (response.getStatus() == HttpStatus.NOT_FOUND_404) {
235 throw new BondException("@text/offline.comm-error.device-not-found");
237 // all api responses return Json. If we get something else it must
238 // be an error message, e.g. http result code
239 if (!httpResponse.startsWith("{") && !httpResponse.startsWith("[")) {
240 throw new BondException("@text/offline.comm-error.unexpected-response");
243 logger.debug("HTTP response from request to {}: {}", uri, httpResponse);
245 } catch (InterruptedException | TimeoutException | ExecutionException e) {
246 logger.debug("Last request to Bond Bridge failed; {} retries remaining: {}", numRetriesRemaining,
248 numRetriesRemaining--;
249 if (numRetriesRemaining == 0) {
250 logger.debug("Repeated Bond API calls to {} failed.", uri);
251 bridgeHandler.setBridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR,
252 "@text/offline.comm-error.api-call-failed");
253 throw new BondException("@text/offline.conf-error.api-call-failed", true);