2 * Copyright (c) 2010-2022 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.ShadeCalibrate;
30 import org.openhab.binding.hdpowerview.internal.api.requests.ShadeMove;
31 import org.openhab.binding.hdpowerview.internal.api.requests.ShadeStop;
32 import org.openhab.binding.hdpowerview.internal.api.responses.FirmwareVersion;
33 import org.openhab.binding.hdpowerview.internal.api.responses.FirmwareVersions;
34 import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
35 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
36 import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents;
37 import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
38 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
39 import org.openhab.binding.hdpowerview.internal.api.responses.Survey;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import com.google.gson.Gson;
44 import com.google.gson.JsonObject;
45 import com.google.gson.JsonParseException;
46 import com.google.gson.JsonParser;
49 * JAX-RS targets for communicating with an HD PowerView hub
51 * @author Andy Lintner - Initial contribution
52 * @author Andrew Fiddian-Green - Added support for secondary rail positions
53 * @author Jacob Laursen - Added support for scene groups and automations
56 public class HDPowerViewWebTargets {
58 private final Logger logger = LoggerFactory.getLogger(HDPowerViewWebTargets.class);
61 * the hub returns a 423 error (resource locked) daily just after midnight;
62 * which means it is temporarily undergoing maintenance; so we use "soft"
63 * exception handling during the five minute maintenance period after a 423
66 private final int maintenancePeriod = 300;
67 private Instant maintenanceScheduledEnd = Instant.now().minusSeconds(2 * maintenancePeriod);
69 private final String base;
70 private final String firmwareVersion;
71 private final String shades;
72 private final String sceneActivate;
73 private final String scenes;
74 private final String sceneCollectionActivate;
75 private final String sceneCollections;
76 private final String scheduledEvents;
78 private final Gson gson = new Gson();
79 private final HttpClient httpClient;
82 * private helper class for passing http url query parameters
84 private static class Query {
85 private final String key;
86 private final String value;
88 private Query(String key, String value) {
93 public static Query of(String key, String value) {
94 return new Query(key, value);
97 public String getKey() {
101 public String getValue() {
106 public String toString() {
107 return String.format("?%s=%s", key, value);
112 * Initialize the web targets
114 * @param httpClient the HTTP client (the binding)
115 * @param ipAddress the IP address of the server (the hub)
117 public HDPowerViewWebTargets(HttpClient httpClient, String ipAddress) {
118 base = "http://" + ipAddress + "/api/";
119 shades = base + "shades/";
120 firmwareVersion = base + "fwversion/";
121 sceneActivate = base + "scenes";
122 scenes = base + "scenes/";
124 // Hub v1 only supports "scenecollections". Hub v2 will redirect to "sceneCollections".
125 sceneCollectionActivate = base + "scenecollections";
126 sceneCollections = base + "scenecollections/";
128 scheduledEvents = base + "scheduledevents";
129 this.httpClient = httpClient;
133 * Fetches a JSON package with firmware information for the hub.
135 * @return FirmwareVersions class instance
136 * @throws JsonParseException if there is a JSON parsing error
137 * @throws HubProcessingException if there is any processing error
138 * @throws HubMaintenanceException if the hub is down for maintenance
140 public FirmwareVersions getFirmwareVersions()
141 throws JsonParseException, HubProcessingException, HubMaintenanceException {
142 String json = invoke(HttpMethod.GET, firmwareVersion, null, null);
143 FirmwareVersion firmwareVersion = gson.fromJson(json, FirmwareVersion.class);
144 if (firmwareVersion == null) {
145 throw new JsonParseException("Missing firmware response");
147 FirmwareVersions firmwareVersions = firmwareVersion.firmware;
148 if (firmwareVersions == null) {
149 throw new JsonParseException("Missing 'firmware' element");
151 return firmwareVersions;
155 * Fetches a JSON package that describes all shades in the hub, and wraps it in
156 * a Shades class instance
158 * @return Shades class instance
159 * @throws JsonParseException if there is a JSON parsing error
160 * @throws HubProcessingException if there is any processing error
161 * @throws HubMaintenanceException if the hub is down for maintenance
163 public @Nullable Shades getShades() throws JsonParseException, HubProcessingException, HubMaintenanceException {
164 String json = invoke(HttpMethod.GET, shades, null, null);
165 return gson.fromJson(json, Shades.class);
169 * Instructs the hub to move a specific shade
171 * @param shadeId id of the shade to be moved
172 * @param position instance of ShadePosition containing the new position
173 * @return Shade class instance (with new position)
174 * @throws HubProcessingException if there is any processing error
175 * @throws HubMaintenanceException if the hub is down for maintenance
177 public @Nullable Shade moveShade(int shadeId, ShadePosition position)
178 throws JsonParseException, HubProcessingException, HubMaintenanceException {
179 String jsonRequest = gson.toJson(new ShadeMove(position));
180 String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest);
181 return gson.fromJson(jsonResponse, Shade.class);
185 * Instructs the hub to stop movement of a specific shade
187 * @param shadeId id of the shade to be stopped
188 * @return Shade class instance (new position cannot be relied upon)
189 * @throws HubProcessingException if there is any processing error
190 * @throws HubMaintenanceException if the hub is down for maintenance
192 public @Nullable Shade stopShade(int shadeId)
193 throws JsonParseException, HubProcessingException, HubMaintenanceException {
194 String jsonRequest = gson.toJson(new ShadeStop());
195 String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest);
196 return gson.fromJson(jsonResponse, Shade.class);
200 * Instructs the hub to calibrate a specific shade
202 * @param shadeId id of the shade to be calibrated
203 * @return Shade class instance
204 * @throws HubProcessingException if there is any processing error
205 * @throws HubMaintenanceException if the hub is down for maintenance
207 public @Nullable Shade calibrateShade(int shadeId)
208 throws JsonParseException, HubProcessingException, HubMaintenanceException {
209 String jsonRequest = gson.toJson(new ShadeCalibrate());
210 String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest);
211 return gson.fromJson(jsonResponse, Shade.class);
215 * Fetches a JSON package that describes all scenes in the hub, and wraps it in
216 * a Scenes class instance
218 * @return Scenes class instance
219 * @throws JsonParseException if there is a JSON parsing error
220 * @throws HubProcessingException if there is any processing error
221 * @throws HubMaintenanceException if the hub is down for maintenance
223 public @Nullable Scenes getScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
224 String json = invoke(HttpMethod.GET, scenes, null, null);
225 return gson.fromJson(json, Scenes.class);
229 * Instructs the hub to execute a specific scene
231 * @param sceneId id of the scene to be executed
232 * @throws HubProcessingException if there is any processing error
233 * @throws HubMaintenanceException if the hub is down for maintenance
235 public void activateScene(int sceneId) throws HubProcessingException, HubMaintenanceException {
236 invoke(HttpMethod.GET, sceneActivate, Query.of("sceneId", Integer.toString(sceneId)), null);
240 * Fetches a JSON package that describes all scene collections in the hub, and wraps it in
241 * a SceneCollections class instance
243 * @return SceneCollections class instance
244 * @throws JsonParseException if there is a JSON parsing error
245 * @throws HubProcessingException if there is any processing error
246 * @throws HubMaintenanceException if the hub is down for maintenance
248 public @Nullable SceneCollections getSceneCollections()
249 throws JsonParseException, HubProcessingException, HubMaintenanceException {
250 String json = invoke(HttpMethod.GET, sceneCollections, null, null);
251 return gson.fromJson(json, SceneCollections.class);
255 * Instructs the hub to execute a specific scene collection
257 * @param sceneCollectionId id of the scene collection to be executed
258 * @throws HubProcessingException if there is any processing error
259 * @throws HubMaintenanceException if the hub is down for maintenance
261 public void activateSceneCollection(int sceneCollectionId) throws HubProcessingException, HubMaintenanceException {
262 invoke(HttpMethod.GET, sceneCollectionActivate,
263 Query.of("sceneCollectionId", Integer.toString(sceneCollectionId)), null);
267 * Fetches a JSON package that describes all scheduled events in the hub, and wraps it in
268 * a ScheduledEvents class instance
270 * @return ScheduledEvents class instance
271 * @throws JsonParseException if there is a JSON parsing error
272 * @throws HubProcessingException if there is any processing error
273 * @throws HubMaintenanceException if the hub is down for maintenance
275 public @Nullable ScheduledEvents getScheduledEvents()
276 throws JsonParseException, HubProcessingException, HubMaintenanceException {
277 String json = invoke(HttpMethod.GET, scheduledEvents, null, null);
278 return gson.fromJson(json, ScheduledEvents.class);
282 * Enables or disables a scheduled event in the hub.
284 * @param scheduledEventId id of the scheduled event to be enabled or disabled
285 * @param enable true to enable scheduled event, false to disable
286 * @throws JsonParseException if there is a JSON parsing error
287 * @throws JsonSyntaxException if there is a JSON syntax error
288 * @throws HubProcessingException if there is any processing error
289 * @throws HubMaintenanceException if the hub is down for maintenance
291 public void enableScheduledEvent(int scheduledEventId, boolean enable)
292 throws JsonParseException, HubProcessingException, HubMaintenanceException {
293 String uri = scheduledEvents + "/" + scheduledEventId;
294 String json = invoke(HttpMethod.GET, uri, null, null);
295 JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
296 JsonObject scheduledEventObject = jsonObject.get("scheduledEvent").getAsJsonObject();
297 scheduledEventObject.addProperty("enabled", enable);
298 invoke(HttpMethod.PUT, uri, null, jsonObject.toString());
302 * Invoke a call on the hub server to retrieve information or send a command
304 * @param method GET or PUT
305 * @param url the host url to be called
306 * @param query the http query parameter
307 * @param jsonCommand the request command content (as a json string)
308 * @return the response content (as a json string)
309 * @throws HubMaintenanceException
310 * @throws HubProcessingException
312 private synchronized String invoke(HttpMethod method, String url, @Nullable Query query,
313 @Nullable String jsonCommand) throws HubMaintenanceException, HubProcessingException {
314 if (logger.isTraceEnabled()) {
316 logger.trace("API command {} {}{}", method, url, query);
318 logger.trace("API command {} {}", method, url);
320 if (jsonCommand != null) {
321 logger.trace("JSON command = {}", jsonCommand);
324 Request request = httpClient.newRequest(url).method(method).header("Connection", "close").accept("*/*");
326 request.param(query.getKey(), query.getValue());
328 if (jsonCommand != null) {
329 request.header(HttpHeader.CONTENT_TYPE, "application/json").content(new StringContentProvider(jsonCommand));
331 ContentResponse response;
333 response = request.send();
334 } catch (InterruptedException | TimeoutException | ExecutionException e) {
335 if (Instant.now().isBefore(maintenanceScheduledEnd)) {
336 // throw "softer" exception during maintenance window
337 logger.debug("Hub still undergoing maintenance");
338 throw new HubMaintenanceException("Hub still undergoing maintenance");
340 throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
342 int statusCode = response.getStatus();
343 if (statusCode == HttpStatus.LOCKED_423) {
344 // set end of maintenance window, and throw a "softer" exception
345 maintenanceScheduledEnd = Instant.now().plusSeconds(maintenancePeriod);
346 logger.debug("Hub undergoing maintenance");
347 throw new HubMaintenanceException("Hub undergoing maintenance");
349 if (statusCode != HttpStatus.OK_200) {
350 logger.warn("Hub returned HTTP {} {}", statusCode, response.getReason());
351 throw new HubProcessingException(String.format("HTTP %d error", statusCode));
353 String jsonResponse = response.getContentAsString();
354 if ("".equals(jsonResponse)) {
355 logger.warn("Hub returned no content");
356 throw new HubProcessingException("Missing response entity");
358 if (logger.isTraceEnabled()) {
359 logger.trace("JSON response = {}", jsonResponse);
365 * Fetches a JSON package that describes a specific shade in the hub, and wraps it
366 * in a Shade class instance
368 * @param shadeId id of the shade to be fetched
369 * @return Shade class instance
370 * @throws JsonParseException if there is a JSON parsing error
371 * @throws HubProcessingException if there is any processing error
372 * @throws HubMaintenanceException if the hub is down for maintenance
374 public @Nullable Shade getShade(int shadeId)
375 throws JsonParseException, HubProcessingException, HubMaintenanceException {
376 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), null, null);
377 return gson.fromJson(json, Shade.class);
381 * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
382 * a specific shade's position; fetches a JSON package that describes that shade,
383 * and wraps it in a Shade class instance
385 * @param shadeId id of the shade to be refreshed
386 * @return Shade class instance
387 * @throws JsonParseException if there is a JSON parsing error
388 * @throws HubProcessingException if there is any processing error
389 * @throws HubMaintenanceException if the hub is down for maintenance
391 public @Nullable Shade refreshShadePosition(int shadeId)
392 throws JsonParseException, HubProcessingException, HubMaintenanceException {
393 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
394 Query.of("refresh", Boolean.toString(true)), null);
395 return gson.fromJson(json, Shade.class);
399 * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
400 * a specific shade's survey data, which will also refresh signal strength;
401 * fetches a JSON package that describes that survey, and wraps it in a Survey
404 * @param shadeId id of the shade to be surveyed
405 * @return Survey class instance
406 * @throws JsonParseException if there is a JSON parsing error
407 * @throws HubProcessingException if there is any processing error
408 * @throws HubMaintenanceException if the hub is down for maintenance
410 public @Nullable Survey getShadeSurvey(int shadeId)
411 throws JsonParseException, HubProcessingException, HubMaintenanceException {
412 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
413 Query.of("survey", Boolean.toString(true)), null);
414 return gson.fromJson(json, Survey.class);
418 * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
419 * a specific shade's battery level; fetches a JSON package that describes that shade,
420 * and wraps it in a Shade class instance
422 * @param shadeId id of the shade to be refreshed
423 * @return Shade class instance
424 * @throws JsonParseException if there is a JSON parsing error
425 * @throws HubProcessingException if there is any processing error
426 * @throws HubMaintenanceException if the hub is down for maintenance
428 public @Nullable Shade refreshShadeBatteryLevel(int shadeId)
429 throws JsonParseException, HubProcessingException, HubMaintenanceException {
430 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
431 Query.of("updateBatteryLevel", Boolean.toString(true)), null);
432 return gson.fromJson(json, Shade.class);