2 * Copyright (c) 2010-2021 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.hdpowerview.internal;
15 import java.time.Instant;
16 import java.util.concurrent.ExecutionException;
17 import java.util.concurrent.TimeoutException;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.eclipse.jetty.client.HttpClient;
22 import org.eclipse.jetty.client.api.ContentResponse;
23 import org.eclipse.jetty.client.api.Request;
24 import org.eclipse.jetty.client.util.StringContentProvider;
25 import org.eclipse.jetty.http.HttpHeader;
26 import org.eclipse.jetty.http.HttpMethod;
27 import org.eclipse.jetty.http.HttpStatus;
28 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
29 import org.openhab.binding.hdpowerview.internal.api.requests.ShadeMove;
30 import org.openhab.binding.hdpowerview.internal.api.requests.ShadeStop;
31 import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
32 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
33 import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
34 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
38 import com.google.gson.Gson;
39 import com.google.gson.JsonParseException;
42 * JAX-RS targets for communicating with an HD PowerView hub
44 * @author Andy Lintner - Initial contribution
45 * @author Andrew Fiddian-Green - Added support for secondary rail positions
46 * @author Jacob Laursen - Add support for scene groups
49 public class HDPowerViewWebTargets {
51 private final Logger logger = LoggerFactory.getLogger(HDPowerViewWebTargets.class);
54 * the hub returns a 423 error (resource locked) daily just after midnight;
55 * which means it is temporarily undergoing maintenance; so we use "soft"
56 * exception handling during the five minute maintenance period after a 423
59 private final int maintenancePeriod = 300;
60 private Instant maintenanceScheduledEnd = Instant.now().minusSeconds(2 * maintenancePeriod);
62 private final String base;
63 private final String shades;
64 private final String sceneActivate;
65 private final String scenes;
66 private final String sceneCollectionActivate;
67 private final String sceneCollections;
69 private final Gson gson = new Gson();
70 private final HttpClient httpClient;
73 * private helper class for passing http url query parameters
75 private static class Query {
76 private final String key;
77 private final String value;
79 private Query(String key, String value) {
84 public static Query of(String key, String value) {
85 return new Query(key, value);
88 public String getKey() {
92 public String getValue() {
98 * Initialize the web targets
100 * @param httpClient the HTTP client (the binding)
101 * @param ipAddress the IP address of the server (the hub)
103 public HDPowerViewWebTargets(HttpClient httpClient, String ipAddress) {
104 base = "http://" + ipAddress + "/api/";
105 shades = base + "shades/";
106 sceneActivate = base + "scenes";
107 scenes = base + "scenes/";
108 sceneCollectionActivate = base + "sceneCollections";
109 sceneCollections = base + "sceneCollections/";
110 this.httpClient = httpClient;
114 * Fetches a JSON package that describes all shades in the hub, and wraps it in
115 * a Shades class instance
117 * @return Shades class instance
118 * @throws JsonParseException if there is a JSON parsing error
119 * @throws HubProcessingException if there is any processing error
120 * @throws HubMaintenanceException if the hub is down for maintenance
122 public @Nullable Shades getShades() throws JsonParseException, HubProcessingException, HubMaintenanceException {
123 String json = invoke(HttpMethod.GET, shades, null, null);
124 return gson.fromJson(json, Shades.class);
128 * Instructs the hub to move a specific shade
130 * @param shadeId id of the shade to be moved
131 * @param position instance of ShadePosition containing the new position
132 * @throws HubProcessingException if there is any processing error
133 * @throws HubMaintenanceException if the hub is down for maintenance
135 public void moveShade(int shadeId, ShadePosition position) throws HubProcessingException, HubMaintenanceException {
136 String json = gson.toJson(new ShadeMove(shadeId, position));
137 invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json);
141 * Fetches a JSON package that describes all scenes in the hub, and wraps it in
142 * a Scenes class instance
144 * @return Scenes class instance
145 * @throws JsonParseException if there is a JSON parsing error
146 * @throws HubProcessingException if there is any processing error
147 * @throws HubMaintenanceException if the hub is down for maintenance
149 public @Nullable Scenes getScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
150 String json = invoke(HttpMethod.GET, scenes, null, null);
151 return gson.fromJson(json, Scenes.class);
155 * Instructs the hub to execute a specific scene
157 * @param sceneId id of the scene to be executed
158 * @throws HubProcessingException if there is any processing error
159 * @throws HubMaintenanceException if the hub is down for maintenance
161 public void activateScene(int sceneId) throws HubProcessingException, HubMaintenanceException {
162 invoke(HttpMethod.GET, sceneActivate, Query.of("sceneId", Integer.toString(sceneId)), null);
166 * Fetches a JSON package that describes all scene collections in the hub, and wraps it in
167 * a SceneCollections class instance
169 * @return SceneCollections class instance
170 * @throws JsonParseException if there is a JSON parsing error
171 * @throws HubProcessingException if there is any processing error
172 * @throws HubMaintenanceException if the hub is down for maintenance
174 public @Nullable SceneCollections getSceneCollections()
175 throws JsonParseException, HubProcessingException, HubMaintenanceException {
176 String json = invoke(HttpMethod.GET, sceneCollections, null, null);
177 return gson.fromJson(json, SceneCollections.class);
181 * Instructs the hub to execute a specific scene collection
183 * @param sceneCollectionId id of the scene collection to be executed
184 * @throws HubProcessingException if there is any processing error
185 * @throws HubMaintenanceException if the hub is down for maintenance
187 public void activateSceneCollection(int sceneCollectionId) throws HubProcessingException, HubMaintenanceException {
188 invoke(HttpMethod.GET, sceneCollectionActivate,
189 Query.of("sceneCollectionId", Integer.toString(sceneCollectionId)), null);
193 * Invoke a call on the hub server to retrieve information or send a command
195 * @param method GET or PUT
196 * @param url the host url to be called
197 * @param query the http query parameter
198 * @param jsonCommand the request command content (as a json string)
199 * @return the response content (as a json string)
200 * @throws HubProcessingException
201 * @throws HubMaintenanceException
202 * @throws HubProcessingException
204 private synchronized String invoke(HttpMethod method, String url, @Nullable Query query,
205 @Nullable String jsonCommand) throws HubMaintenanceException, HubProcessingException {
206 if (logger.isTraceEnabled()) {
207 logger.trace("API command {} {}", method, url);
208 if (jsonCommand != null) {
209 logger.trace("JSON command = {}", jsonCommand);
212 Request request = httpClient.newRequest(url).method(method).header("Connection", "close").accept("*/*");
214 request.param(query.getKey(), query.getValue());
216 if (jsonCommand != null) {
217 request.header(HttpHeader.CONTENT_TYPE, "application/json").content(new StringContentProvider(jsonCommand));
219 ContentResponse response;
221 response = request.send();
222 } catch (InterruptedException | TimeoutException | ExecutionException e) {
223 if (Instant.now().isBefore(maintenanceScheduledEnd)) {
224 // throw "softer" exception during maintenance window
225 logger.debug("Hub still undergoing maintenance");
226 throw new HubMaintenanceException("Hub still undergoing maintenance");
228 throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
230 int statusCode = response.getStatus();
231 if (statusCode == HttpStatus.LOCKED_423) {
232 // set end of maintenance window, and throw a "softer" exception
233 maintenanceScheduledEnd = Instant.now().plusSeconds(maintenancePeriod);
234 logger.debug("Hub undergoing maintenance");
235 throw new HubMaintenanceException("Hub undergoing maintenance");
237 if (statusCode != HttpStatus.OK_200) {
238 logger.warn("Hub returned HTTP {} {}", statusCode, response.getReason());
239 throw new HubProcessingException(String.format("HTTP %d error", statusCode));
241 String jsonResponse = response.getContentAsString();
242 if ("".equals(jsonResponse)) {
243 logger.warn("Hub returned no content");
244 throw new HubProcessingException("Missing response entity");
246 if (logger.isTraceEnabled()) {
247 logger.trace("JSON response = {}", jsonResponse);
253 * Fetches a JSON package that describes a specific shade in the hub, and wraps it
254 * in a Shade class instance
256 * @param shadeId id of the shade to be fetched
257 * @return Shade class instance
258 * @throws JsonParseException if there is a JSON parsing error
259 * @throws HubProcessingException if there is any processing error
260 * @throws HubMaintenanceException if the hub is down for maintenance
262 public @Nullable Shade getShade(int shadeId)
263 throws JsonParseException, HubProcessingException, HubMaintenanceException {
264 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), null, null);
265 return gson.fromJson(json, Shade.class);
269 * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
270 * a specific shade's position; fetches a JSON package that describes that shade,
271 * and wraps it in a Shade class instance
273 * @param shadeId id of the shade to be refreshed
274 * @return Shade class instance
275 * @throws JsonParseException if there is a JSON parsing error
276 * @throws HubProcessingException if there is any processing error
277 * @throws HubMaintenanceException if the hub is down for maintenance
279 public @Nullable Shade refreshShadePosition(int shadeId)
280 throws JsonParseException, HubProcessingException, HubMaintenanceException {
281 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
282 Query.of("refresh", Boolean.toString(true)), null);
283 return gson.fromJson(json, Shade.class);
287 * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
288 * a specific shade's battery level; fetches a JSON package that describes that shade,
289 * and wraps it in a Shade class instance
291 * @param shadeId id of the shade to be refreshed
292 * @return Shade class instance
293 * @throws JsonParseException if there is a JSON parsing error
294 * @throws HubProcessingException if there is any processing error
295 * @throws HubMaintenanceException if the hub is down for maintenance
297 public @Nullable Shade refreshShadeBatteryLevel(int shadeId)
298 throws JsonParseException, HubProcessingException, HubMaintenanceException {
299 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
300 Query.of("updateBatteryLevel", Boolean.toString(true)), null);
301 return gson.fromJson(json, Shade.class);
305 * Tells the hub to stop movement of a specific shade
307 * @param shadeId id of the shade to be stopped
308 * @throws HubProcessingException if there is any processing error
309 * @throws HubMaintenanceException if the hub is down for maintenance
311 public void stopShade(int shadeId) throws HubProcessingException, HubMaintenanceException {
312 String json = gson.toJson(new ShadeStop(shadeId));
313 invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json);