]> git.basschouten.com Git - openhab-addons.git/blob
97b9ec8880ee6e7fe59bb3f88ac4918f07f546fc
[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.bondhome.internal.api;
14
15 import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
16
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;
23 import java.util.Map;
24 import java.util.Objects;
25 import java.util.Set;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
29
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;
44
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;
50
51 /**
52  * {@link BondHttpApi} wraps the Bond REST API and provides various low
53  * level function to access the device api (not cloud api).
54  *
55  * @author Sara Geleskie Damiano - Initial contribution
56  */
57 @NonNullByDefault
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();
63
64     public BondHttpApi(BondBridgeHandler bridgeHandler, final HttpClientFactory httpClientFactory) {
65         this.bridgeHandler = bridgeHandler;
66         this.httpClientFactory = httpClientFactory;
67     }
68
69     /**
70      * Gets version information about the Bond bridge
71      *
72      * @return the {@link BondSysVersion}
73      * @throws BondException
74      */
75     public BondSysVersion getBridgeVersion() throws BondException {
76         String json = request("/v2/sys/version");
77         logger.trace("BondHome device info : {}", json);
78         try {
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");
83         }
84     }
85
86     /**
87      * Gets a list of the attached devices
88      *
89      * @return an array of device id's
90      * @throws BondException
91      */
92     public List<String> getDevices() throws BondException {
93         List<String> list = new ArrayList<>();
94         String json = request("/v2/devices/");
95         try {
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("_")) {
102                     list.add(key);
103                 }
104             }
105             return list;
106         } catch (JsonParseException e) {
107             logger.debug("Could not parse devices JSON '{}'", json, e);
108             throw new BondException("@text/offline.comm-error.unparseable-response");
109         }
110     }
111
112     /**
113      * Gets basic device information
114      *
115      * @param deviceId The ID of the device
116      * @return the {@link BondDevice}
117      * @throws BondException
118      */
119     public BondDevice getDevice(String deviceId) throws BondException {
120         String json = request("/v2/devices/" + deviceId);
121         logger.trace("BondHome device info : {}", json);
122         try {
123             BondDevice device = Objects.requireNonNull(gson.fromJson(json, BondDevice.class));
124             device.actions.removeIf(Objects::isNull);
125             return device;
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");
129         }
130     }
131
132     /**
133      * Gets the current state of a device
134      *
135      * @param deviceId The ID of the device
136      * @return the {@link BondDeviceState}
137      * @throws BondException
138      */
139     public BondDeviceState getDeviceState(String deviceId) throws BondException {
140         String json = request("/v2/devices/" + deviceId + "/state");
141         logger.trace("BondHome device state : {}", json);
142         try {
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");
147         }
148     }
149
150     /**
151      * Gets the current properties of a device
152      *
153      * @param deviceId The ID of the device
154      * @return the {@link BondDeviceProperties}
155      * @throws BondException
156      */
157     public BondDeviceProperties getDeviceProperties(String deviceId) throws BondException {
158         String json = request("/v2/devices/" + deviceId + "/properties");
159         logger.trace("BondHome device properties : {}", json);
160         try {
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");
165         }
166     }
167
168     /**
169      * Executes a device action
170      *
171      * @param deviceId The ID of the device
172      * @param actionId The Bond action
173      * @param argument An additional argument for the actions (such as the fan speed)
174      */
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 + "}";
181         }
182         InputStream content = new ByteArrayInputStream(payload.getBytes());
183         logger.debug("HTTP PUT to {} with content {}", url, payload);
184
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);
189
190         try (final InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(content)) {
191             request.content(inputStreamContentProvider, "application/json");
192         }
193         ContentResponse response;
194         try {
195             response = request.send();
196         } catch (Exception e) {
197             logger.warn("Unable to execute device action {} against device {}: {}", deviceId, action, e.getMessage());
198             return;
199         }
200
201         logger.debug("HTTP response from {}: {}", deviceId, response.getStatus());
202     }
203
204     /**
205      * Submit GET request and return response, check for invalid responses
206      *
207      * @param uri: URI (e.g. "/settings")
208      */
209     private synchronized String request(String uri) throws BondException {
210         String httpResponse = "ERROR";
211         String url = "http://" + bridgeHandler.getBridgeIpAddress() + uri;
212         int numRetriesRemaining = 3;
213         do {
214             try {
215                 logger.debug("HTTP GET to {}", url);
216
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.send();
222                 String encoding = response.getEncoding() != null ? response.getEncoding().replace("\"", "").trim()
223                         : StandardCharsets.UTF_8.name();
224                 try {
225                     httpResponse = new String(response.getContent(), encoding);
226                 } catch (UnsupportedEncodingException e) {
227                     throw new BondException("@text/offline.comm-error.no-response");
228                 }
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);
235                 }
236                 if (response.getStatus() == HttpStatus.NOT_FOUND_404) {
237                     throw new BondException("@text/offline.comm-error.device-not-found");
238                 }
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");
243                 }
244
245                 logger.debug("HTTP response from request to {}: {}", uri, httpResponse);
246                 return httpResponse;
247             } catch (InterruptedException | TimeoutException | ExecutionException e) {
248                 logger.debug("Last request to Bond Bridge failed; {} retries remaining: {}", numRetriesRemaining,
249                         e.getMessage());
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);
256                 }
257             }
258         } while (true);
259     }
260 }