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.ScheduledEvents;
34 import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
35 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
39 import com.google.gson.Gson;
40 import com.google.gson.JsonObject;
41 import com.google.gson.JsonParseException;
42 import com.google.gson.JsonParser;
45 * JAX-RS targets for communicating with an HD PowerView hub
47 * @author Andy Lintner - Initial contribution
48 * @author Andrew Fiddian-Green - Added support for secondary rail positions
49 * @author Jacob Laursen - Add support for scene groups and automations
52 public class HDPowerViewWebTargets {
54 private final Logger logger = LoggerFactory.getLogger(HDPowerViewWebTargets.class);
57 * the hub returns a 423 error (resource locked) daily just after midnight;
58 * which means it is temporarily undergoing maintenance; so we use "soft"
59 * exception handling during the five minute maintenance period after a 423
62 private final int maintenancePeriod = 300;
63 private Instant maintenanceScheduledEnd = Instant.now().minusSeconds(2 * maintenancePeriod);
65 private final String base;
66 private final String shades;
67 private final String sceneActivate;
68 private final String scenes;
69 private final String sceneCollectionActivate;
70 private final String sceneCollections;
71 private final String scheduledEvents;
73 private final Gson gson = new Gson();
74 private final HttpClient httpClient;
77 * private helper class for passing http url query parameters
79 private static class Query {
80 private final String key;
81 private final String value;
83 private Query(String key, String value) {
88 public static Query of(String key, String value) {
89 return new Query(key, value);
92 public String getKey() {
96 public String getValue() {
102 * Initialize the web targets
104 * @param httpClient the HTTP client (the binding)
105 * @param ipAddress the IP address of the server (the hub)
107 public HDPowerViewWebTargets(HttpClient httpClient, String ipAddress) {
108 base = "http://" + ipAddress + "/api/";
109 shades = base + "shades/";
110 sceneActivate = base + "scenes";
111 scenes = base + "scenes/";
113 // Hub v1 only supports "scenecollections". Hub v2 will redirect to "sceneCollections".
114 sceneCollectionActivate = base + "scenecollections";
115 sceneCollections = base + "scenecollections/";
117 scheduledEvents = base + "scheduledevents";
118 this.httpClient = httpClient;
122 * Fetches a JSON package that describes all shades in the hub, and wraps it in
123 * a Shades class instance
125 * @return Shades class instance
126 * @throws JsonParseException if there is a JSON parsing error
127 * @throws HubProcessingException if there is any processing error
128 * @throws HubMaintenanceException if the hub is down for maintenance
130 public @Nullable Shades getShades() throws JsonParseException, HubProcessingException, HubMaintenanceException {
131 String json = invoke(HttpMethod.GET, shades, null, null);
132 return gson.fromJson(json, Shades.class);
136 * Instructs the hub to move a specific shade
138 * @param shadeId id of the shade to be moved
139 * @param position instance of ShadePosition containing the new position
140 * @throws HubProcessingException if there is any processing error
141 * @throws HubMaintenanceException if the hub is down for maintenance
143 public void moveShade(int shadeId, ShadePosition position) throws HubProcessingException, HubMaintenanceException {
144 String json = gson.toJson(new ShadeMove(shadeId, position));
145 invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json);
149 * Fetches a JSON package that describes all scenes in the hub, and wraps it in
150 * a Scenes class instance
152 * @return Scenes class instance
153 * @throws JsonParseException if there is a JSON parsing error
154 * @throws HubProcessingException if there is any processing error
155 * @throws HubMaintenanceException if the hub is down for maintenance
157 public @Nullable Scenes getScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
158 String json = invoke(HttpMethod.GET, scenes, null, null);
159 return gson.fromJson(json, Scenes.class);
163 * Instructs the hub to execute a specific scene
165 * @param sceneId id of the scene to be executed
166 * @throws HubProcessingException if there is any processing error
167 * @throws HubMaintenanceException if the hub is down for maintenance
169 public void activateScene(int sceneId) throws HubProcessingException, HubMaintenanceException {
170 invoke(HttpMethod.GET, sceneActivate, Query.of("sceneId", Integer.toString(sceneId)), null);
174 * Fetches a JSON package that describes all scene collections in the hub, and wraps it in
175 * a SceneCollections class instance
177 * @return SceneCollections class instance
178 * @throws JsonParseException if there is a JSON parsing error
179 * @throws HubProcessingException if there is any processing error
180 * @throws HubMaintenanceException if the hub is down for maintenance
182 public @Nullable SceneCollections getSceneCollections()
183 throws JsonParseException, HubProcessingException, HubMaintenanceException {
184 String json = invoke(HttpMethod.GET, sceneCollections, null, null);
185 return gson.fromJson(json, SceneCollections.class);
189 * Instructs the hub to execute a specific scene collection
191 * @param sceneCollectionId id of the scene collection to be executed
192 * @throws HubProcessingException if there is any processing error
193 * @throws HubMaintenanceException if the hub is down for maintenance
195 public void activateSceneCollection(int sceneCollectionId) throws HubProcessingException, HubMaintenanceException {
196 invoke(HttpMethod.GET, sceneCollectionActivate,
197 Query.of("sceneCollectionId", Integer.toString(sceneCollectionId)), null);
201 * Fetches a JSON package that describes all scheduled events in the hub, and wraps it in
202 * a ScheduledEvents class instance
204 * @return ScheduledEvents class instance
205 * @throws JsonParseException if there is a JSON parsing error
206 * @throws HubProcessingException if there is any processing error
207 * @throws HubMaintenanceException if the hub is down for maintenance
209 public @Nullable ScheduledEvents getScheduledEvents()
210 throws JsonParseException, HubProcessingException, HubMaintenanceException {
211 String json = invoke(HttpMethod.GET, scheduledEvents, null, null);
212 return gson.fromJson(json, ScheduledEvents.class);
216 * Enables or disables a scheduled event in the hub.
218 * @param scheduledEventId id of the scheduled event to be enabled or disabled
219 * @param enable true to enable scheduled event, false to disable
220 * @throws JsonParseException if there is a JSON parsing error
221 * @throws JsonSyntaxException if there is a JSON syntax error
222 * @throws HubProcessingException if there is any processing error
223 * @throws HubMaintenanceException if the hub is down for maintenance
225 public void enableScheduledEvent(int scheduledEventId, boolean enable)
226 throws JsonParseException, HubProcessingException, HubMaintenanceException {
227 String uri = scheduledEvents + "/" + scheduledEventId;
228 String json = invoke(HttpMethod.GET, uri, null, null);
229 JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
230 JsonObject scheduledEventObject = jsonObject.get("scheduledEvent").getAsJsonObject();
231 scheduledEventObject.addProperty("enabled", enable);
232 invoke(HttpMethod.PUT, uri, null, jsonObject.toString());
236 * Invoke a call on the hub server to retrieve information or send a command
238 * @param method GET or PUT
239 * @param url the host url to be called
240 * @param query the http query parameter
241 * @param jsonCommand the request command content (as a json string)
242 * @return the response content (as a json string)
243 * @throws HubMaintenanceException
244 * @throws HubProcessingException
246 private synchronized String invoke(HttpMethod method, String url, @Nullable Query query,
247 @Nullable String jsonCommand) throws HubMaintenanceException, HubProcessingException {
248 if (logger.isTraceEnabled()) {
249 logger.trace("API command {} {}", method, url);
250 if (jsonCommand != null) {
251 logger.trace("JSON command = {}", jsonCommand);
254 Request request = httpClient.newRequest(url).method(method).header("Connection", "close").accept("*/*");
256 request.param(query.getKey(), query.getValue());
258 if (jsonCommand != null) {
259 request.header(HttpHeader.CONTENT_TYPE, "application/json").content(new StringContentProvider(jsonCommand));
261 ContentResponse response;
263 response = request.send();
264 } catch (InterruptedException | TimeoutException | ExecutionException e) {
265 if (Instant.now().isBefore(maintenanceScheduledEnd)) {
266 // throw "softer" exception during maintenance window
267 logger.debug("Hub still undergoing maintenance");
268 throw new HubMaintenanceException("Hub still undergoing maintenance");
270 throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
272 int statusCode = response.getStatus();
273 if (statusCode == HttpStatus.LOCKED_423) {
274 // set end of maintenance window, and throw a "softer" exception
275 maintenanceScheduledEnd = Instant.now().plusSeconds(maintenancePeriod);
276 logger.debug("Hub undergoing maintenance");
277 throw new HubMaintenanceException("Hub undergoing maintenance");
279 if (statusCode != HttpStatus.OK_200) {
280 logger.warn("Hub returned HTTP {} {}", statusCode, response.getReason());
281 throw new HubProcessingException(String.format("HTTP %d error", statusCode));
283 String jsonResponse = response.getContentAsString();
284 if ("".equals(jsonResponse)) {
285 logger.warn("Hub returned no content");
286 throw new HubProcessingException("Missing response entity");
288 if (logger.isTraceEnabled()) {
289 logger.trace("JSON response = {}", jsonResponse);
295 * Fetches a JSON package that describes a specific shade in the hub, and wraps it
296 * in a Shade class instance
298 * @param shadeId id of the shade to be fetched
299 * @return Shade class instance
300 * @throws JsonParseException if there is a JSON parsing error
301 * @throws HubProcessingException if there is any processing error
302 * @throws HubMaintenanceException if the hub is down for maintenance
304 public @Nullable Shade getShade(int shadeId)
305 throws JsonParseException, HubProcessingException, HubMaintenanceException {
306 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), null, null);
307 return gson.fromJson(json, Shade.class);
311 * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
312 * a specific shade's position; fetches a JSON package that describes that shade,
313 * and wraps it in a Shade class instance
315 * @param shadeId id of the shade to be refreshed
316 * @return Shade class instance
317 * @throws JsonParseException if there is a JSON parsing error
318 * @throws HubProcessingException if there is any processing error
319 * @throws HubMaintenanceException if the hub is down for maintenance
321 public @Nullable Shade refreshShadePosition(int shadeId)
322 throws JsonParseException, HubProcessingException, HubMaintenanceException {
323 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
324 Query.of("refresh", Boolean.toString(true)), null);
325 return gson.fromJson(json, Shade.class);
329 * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
330 * a specific shade's battery level; fetches a JSON package that describes that shade,
331 * and wraps it in a Shade class instance
333 * @param shadeId id of the shade to be refreshed
334 * @return Shade class instance
335 * @throws JsonParseException if there is a JSON parsing error
336 * @throws HubProcessingException if there is any processing error
337 * @throws HubMaintenanceException if the hub is down for maintenance
339 public @Nullable Shade refreshShadeBatteryLevel(int shadeId)
340 throws JsonParseException, HubProcessingException, HubMaintenanceException {
341 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
342 Query.of("updateBatteryLevel", Boolean.toString(true)), null);
343 return gson.fromJson(json, Shade.class);
347 * Tells the hub to stop movement of a specific shade
349 * @param shadeId id of the shade to be stopped
350 * @throws HubProcessingException if there is any processing error
351 * @throws HubMaintenanceException if the hub is down for maintenance
353 public void stopShade(int shadeId) throws HubProcessingException, HubMaintenanceException {
354 String json = gson.toJson(new ShadeStop(shadeId));
355 invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json);