]> git.basschouten.com Git - openhab-addons.git/blob
c9fd6cd22e8572c596e2e541bee77b20ea470b28
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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             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");
127         }
128     }
129
130     /**
131      * Gets the current state of a device
132      *
133      * @param deviceId The ID of the device
134      * @return the {@link BondDeviceState}
135      * @throws BondException
136      */
137     public BondDeviceState getDeviceState(String deviceId) throws BondException {
138         String json = request("/v2/devices/" + deviceId + "/state");
139         logger.trace("BondHome device state : {}", json);
140         try {
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");
145         }
146     }
147
148     /**
149      * Gets the current properties of a device
150      *
151      * @param deviceId The ID of the device
152      * @return the {@link BondDeviceProperties}
153      * @throws BondException
154      */
155     public BondDeviceProperties getDeviceProperties(String deviceId) throws BondException {
156         String json = request("/v2/devices/" + deviceId + "/properties");
157         logger.trace("BondHome device properties : {}", json);
158         try {
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");
163         }
164     }
165
166     /**
167      * Executes a device action
168      *
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)
172      */
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 + "}";
179         }
180         InputStream content = new ByteArrayInputStream(payload.getBytes());
181         logger.debug("HTTP PUT to {} with content {}", url, payload);
182
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);
187
188         try (final InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(content)) {
189             request.content(inputStreamContentProvider, "application/json");
190         }
191         ContentResponse response;
192         try {
193             response = request.send();
194         } catch (Exception e) {
195             logger.warn("Unable to execute device action {} against device {}: {}", deviceId, action, e.getMessage());
196             return;
197         }
198
199         logger.debug("HTTP response from {}: {}", deviceId, response.getStatus());
200     }
201
202     /**
203      * Submit GET request and return response, check for invalid responses
204      *
205      * @param uri: URI (e.g. "/settings")
206      */
207     private synchronized String request(String uri) throws BondException {
208         String httpResponse = "ERROR";
209         String url = "http://" + bridgeHandler.getBridgeIpAddress() + uri;
210         int numRetriesRemaining = 3;
211         do {
212             try {
213                 logger.debug("HTTP GET to {}", url);
214
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();
222                 try {
223                     httpResponse = new String(response.getContent(), encoding);
224                 } catch (UnsupportedEncodingException e) {
225                     throw new BondException("@text/offline.comm-error.no-response");
226                 }
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);
233                 }
234                 if (response.getStatus() == HttpStatus.NOT_FOUND_404) {
235                     throw new BondException("@text/offline.comm-error.device-not-found");
236                 }
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");
241                 }
242
243                 logger.debug("HTTP response from request to {}: {}", uri, httpResponse);
244                 return httpResponse;
245             } catch (InterruptedException | TimeoutException | ExecutionException e) {
246                 logger.debug("Last request to Bond Bridge failed; {} retries remaining: {}", numRetriesRemaining,
247                         e.getMessage());
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);
254                 }
255             }
256         } while (true);
257     }
258 }