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.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 BondDevice device = Objects.requireNonNull(gson.fromJson(json, BondDevice.class));
124 device.actions.removeIf(Objects::isNull);
126 } catch (JsonParseException e) {
127 logger.debug("Could not parse device {}'s JSON '{}'", deviceId, json, e);
128 throw new BondException("@text/offline.comm-error.unparseable-response");
133 * Gets the current state of a device
135 * @param deviceId The ID of the device
136 * @return the {@link BondDeviceState}
137 * @throws BondException
139 public BondDeviceState getDeviceState(String deviceId) throws BondException {
140 String json = request("/v2/devices/" + deviceId + "/state");
141 logger.trace("BondHome device state : {}", json);
143 return Objects.requireNonNull(gson.fromJson(json, BondDeviceState.class));
144 } catch (JsonParseException e) {
145 logger.debug("Could not parse device {}'s state JSON '{}'", deviceId, json, e);
146 throw new BondException("@text/offline.comm-error.unparseable-response");
151 * Gets the current properties of a device
153 * @param deviceId The ID of the device
154 * @return the {@link BondDeviceProperties}
155 * @throws BondException
157 public BondDeviceProperties getDeviceProperties(String deviceId) throws BondException {
158 String json = request("/v2/devices/" + deviceId + "/properties");
159 logger.trace("BondHome device properties : {}", json);
161 return Objects.requireNonNull(gson.fromJson(json, BondDeviceProperties.class));
162 } catch (JsonParseException e) {
163 logger.debug("Could not parse device {}'s property JSON '{}'", deviceId, json, e);
164 throw new BondException("@text/offline.comm-error.unparseable-response");
169 * Executes a device action
171 * @param deviceId The ID of the device
172 * @param action The Bond action
173 * @param argument An additional argument for the actions (such as the fan speed)
175 public synchronized void executeDeviceAction(String deviceId, BondDeviceAction action, @Nullable Integer argument) {
176 String url = "http://" + bridgeHandler.getBridgeIpAddress() + "/v2/devices/" + deviceId + "/actions/"
177 + action.getActionId();
178 String payload = "{}";
179 if (argument != null) {
180 payload = "{\"argument\":" + argument + "}";
182 InputStream content = new ByteArrayInputStream(payload.getBytes());
183 logger.debug("HTTP PUT to {} with content {}", url, payload);
185 final HttpClient httpClient = httpClientFactory.getCommonHttpClient();
186 final Request request = httpClient.newRequest(url).method(HttpMethod.PUT)
187 .header("BOND-Token", bridgeHandler.getBridgeToken())
188 .timeout(BOND_API_TIMEOUT_MS, TimeUnit.MILLISECONDS);
190 try (final InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(content)) {
191 request.content(inputStreamContentProvider, "application/json");
193 ContentResponse response;
195 response = request.send();
196 } catch (Exception e) {
197 logger.warn("Unable to execute device action {} against device {}: {}", deviceId, action, e.getMessage());
201 logger.debug("HTTP response from {}: {}", deviceId, response.getStatus());
205 * Submit GET request and return response, check for invalid responses
207 * @param uri: URI (e.g. "/settings")
209 private synchronized String request(String uri) throws BondException {
210 String httpResponse = "ERROR";
211 String url = "http://" + bridgeHandler.getBridgeIpAddress() + uri;
212 int numRetriesRemaining = 3;
215 logger.debug("HTTP GET to {}", url);
217 final HttpClient httpClient = httpClientFactory.getCommonHttpClient();
218 final Request request = httpClient.newRequest(url).method(HttpMethod.GET).header("BOND-Token",
219 bridgeHandler.getBridgeToken());
220 ContentResponse response;
221 response = request.timeout(BOND_API_TIMEOUT_MS, TimeUnit.MILLISECONDS).send();
222 String encoding = response.getEncoding() != null ? response.getEncoding().replace("\"", "").trim()
223 : StandardCharsets.UTF_8.name();
225 httpResponse = new String(response.getContent(), encoding);
226 } catch (UnsupportedEncodingException e) {
227 throw new BondException("@text/offline.comm-error.no-response");
229 // handle known errors
230 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
231 // Don't retry or throw an exception if we get unauthorized, just set the bridge offline
232 bridgeHandler.setBridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR,
233 "@text/offline.conf-error.incorrect-local-token");
234 throw new BondException("@text/offline.conf-error.incorrect-local-token", true);
236 if (response.getStatus() == HttpStatus.NOT_FOUND_404) {
237 throw new BondException("@text/offline.comm-error.device-not-found");
239 // all api responses return Json. If we get something else it must
240 // be an error message, e.g. http result code
241 if (!httpResponse.startsWith("{") && !httpResponse.startsWith("[")) {
242 throw new BondException("@text/offline.comm-error.unexpected-response");
245 logger.debug("HTTP response from request to {}: {}", uri, httpResponse);
247 } catch (InterruptedException | TimeoutException | ExecutionException e) {
248 logger.debug("Last request to Bond Bridge failed; {} retries remaining: {}", numRetriesRemaining,
250 numRetriesRemaining--;
251 if (numRetriesRemaining == 0) {
252 logger.debug("Repeated Bond API calls to {} failed.", uri);
253 bridgeHandler.setBridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR,
254 "@text/offline.comm-error.api-call-failed");
255 throw new BondException("@text/offline.comm-error.api-call-failed", true);