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.ShadeMove;
30 import org.openhab.binding.hdpowerview.internal.api.requests.ShadeStop;
31 import org.openhab.binding.hdpowerview.internal.api.responses.FirmwareVersion;
32 import org.openhab.binding.hdpowerview.internal.api.responses.FirmwareVersions;
33 import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
34 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
35 import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents;
36 import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
37 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
38 import org.openhab.binding.hdpowerview.internal.api.responses.Survey;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
42 import com.google.gson.Gson;
43 import com.google.gson.JsonObject;
44 import com.google.gson.JsonParseException;
45 import com.google.gson.JsonParser;
48 * JAX-RS targets for communicating with an HD PowerView hub
50 * @author Andy Lintner - Initial contribution
51 * @author Andrew Fiddian-Green - Added support for secondary rail positions
52 * @author Jacob Laursen - Added support for scene groups and automations
55 public class HDPowerViewWebTargets {
57 private final Logger logger = LoggerFactory.getLogger(HDPowerViewWebTargets.class);
60 * the hub returns a 423 error (resource locked) daily just after midnight;
61 * which means it is temporarily undergoing maintenance; so we use "soft"
62 * exception handling during the five minute maintenance period after a 423
65 private final int maintenancePeriod = 300;
66 private Instant maintenanceScheduledEnd = Instant.now().minusSeconds(2 * maintenancePeriod);
68 private final String base;
69 private final String firmwareVersion;
70 private final String shades;
71 private final String sceneActivate;
72 private final String scenes;
73 private final String sceneCollectionActivate;
74 private final String sceneCollections;
75 private final String scheduledEvents;
77 private final Gson gson = new Gson();
78 private final HttpClient httpClient;
81 * private helper class for passing http url query parameters
83 private static class Query {
84 private final String key;
85 private final String value;
87 private Query(String key, String value) {
92 public static Query of(String key, String value) {
93 return new Query(key, value);
96 public String getKey() {
100 public String getValue() {
105 public String toString() {
106 return String.format("?%s=%s", key, value);
111 * Initialize the web targets
113 * @param httpClient the HTTP client (the binding)
114 * @param ipAddress the IP address of the server (the hub)
116 public HDPowerViewWebTargets(HttpClient httpClient, String ipAddress) {
117 base = "http://" + ipAddress + "/api/";
118 shades = base + "shades/";
119 firmwareVersion = base + "fwversion/";
120 sceneActivate = base + "scenes";
121 scenes = base + "scenes/";
123 // Hub v1 only supports "scenecollections". Hub v2 will redirect to "sceneCollections".
124 sceneCollectionActivate = base + "scenecollections";
125 sceneCollections = base + "scenecollections/";
127 scheduledEvents = base + "scheduledevents";
128 this.httpClient = httpClient;
132 * Fetches a JSON package with firmware information for the hub.
134 * @return FirmwareVersions class instance
135 * @throws JsonParseException if there is a JSON parsing error
136 * @throws HubProcessingException if there is any processing error
137 * @throws HubMaintenanceException if the hub is down for maintenance
139 public FirmwareVersions getFirmwareVersions()
140 throws JsonParseException, HubProcessingException, HubMaintenanceException {
141 String json = invoke(HttpMethod.GET, firmwareVersion, null, null);
142 FirmwareVersion firmwareVersion = gson.fromJson(json, FirmwareVersion.class);
143 if (firmwareVersion == null) {
144 throw new JsonParseException("Missing firmware response");
146 FirmwareVersions firmwareVersions = firmwareVersion.firmware;
147 if (firmwareVersions == null) {
148 throw new JsonParseException("Missing 'firmware' element");
150 return firmwareVersions;
154 * Fetches a JSON package that describes all shades in the hub, and wraps it in
155 * a Shades class instance
157 * @return Shades class instance
158 * @throws JsonParseException if there is a JSON parsing error
159 * @throws HubProcessingException if there is any processing error
160 * @throws HubMaintenanceException if the hub is down for maintenance
162 public @Nullable Shades getShades() throws JsonParseException, HubProcessingException, HubMaintenanceException {
163 String json = invoke(HttpMethod.GET, shades, null, null);
164 return gson.fromJson(json, Shades.class);
168 * Instructs the hub to move a specific shade
170 * @param shadeId id of the shade to be moved
171 * @param position instance of ShadePosition containing the new position
172 * @throws HubProcessingException if there is any processing error
173 * @throws HubMaintenanceException if the hub is down for maintenance
175 public void moveShade(int shadeId, ShadePosition position) throws HubProcessingException, HubMaintenanceException {
176 String json = gson.toJson(new ShadeMove(shadeId, position));
177 invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json);
181 * Fetches a JSON package that describes all scenes in the hub, and wraps it in
182 * a Scenes class instance
184 * @return Scenes class instance
185 * @throws JsonParseException if there is a JSON parsing error
186 * @throws HubProcessingException if there is any processing error
187 * @throws HubMaintenanceException if the hub is down for maintenance
189 public @Nullable Scenes getScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
190 String json = invoke(HttpMethod.GET, scenes, null, null);
191 return gson.fromJson(json, Scenes.class);
195 * Instructs the hub to execute a specific scene
197 * @param sceneId id of the scene to be executed
198 * @throws HubProcessingException if there is any processing error
199 * @throws HubMaintenanceException if the hub is down for maintenance
201 public void activateScene(int sceneId) throws HubProcessingException, HubMaintenanceException {
202 invoke(HttpMethod.GET, sceneActivate, Query.of("sceneId", Integer.toString(sceneId)), null);
206 * Fetches a JSON package that describes all scene collections in the hub, and wraps it in
207 * a SceneCollections class instance
209 * @return SceneCollections class instance
210 * @throws JsonParseException if there is a JSON parsing error
211 * @throws HubProcessingException if there is any processing error
212 * @throws HubMaintenanceException if the hub is down for maintenance
214 public @Nullable SceneCollections getSceneCollections()
215 throws JsonParseException, HubProcessingException, HubMaintenanceException {
216 String json = invoke(HttpMethod.GET, sceneCollections, null, null);
217 return gson.fromJson(json, SceneCollections.class);
221 * Instructs the hub to execute a specific scene collection
223 * @param sceneCollectionId id of the scene collection to be executed
224 * @throws HubProcessingException if there is any processing error
225 * @throws HubMaintenanceException if the hub is down for maintenance
227 public void activateSceneCollection(int sceneCollectionId) throws HubProcessingException, HubMaintenanceException {
228 invoke(HttpMethod.GET, sceneCollectionActivate,
229 Query.of("sceneCollectionId", Integer.toString(sceneCollectionId)), null);
233 * Fetches a JSON package that describes all scheduled events in the hub, and wraps it in
234 * a ScheduledEvents class instance
236 * @return ScheduledEvents class instance
237 * @throws JsonParseException if there is a JSON parsing error
238 * @throws HubProcessingException if there is any processing error
239 * @throws HubMaintenanceException if the hub is down for maintenance
241 public @Nullable ScheduledEvents getScheduledEvents()
242 throws JsonParseException, HubProcessingException, HubMaintenanceException {
243 String json = invoke(HttpMethod.GET, scheduledEvents, null, null);
244 return gson.fromJson(json, ScheduledEvents.class);
248 * Enables or disables a scheduled event in the hub.
250 * @param scheduledEventId id of the scheduled event to be enabled or disabled
251 * @param enable true to enable scheduled event, false to disable
252 * @throws JsonParseException if there is a JSON parsing error
253 * @throws JsonSyntaxException if there is a JSON syntax error
254 * @throws HubProcessingException if there is any processing error
255 * @throws HubMaintenanceException if the hub is down for maintenance
257 public void enableScheduledEvent(int scheduledEventId, boolean enable)
258 throws JsonParseException, HubProcessingException, HubMaintenanceException {
259 String uri = scheduledEvents + "/" + scheduledEventId;
260 String json = invoke(HttpMethod.GET, uri, null, null);
261 JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
262 JsonObject scheduledEventObject = jsonObject.get("scheduledEvent").getAsJsonObject();
263 scheduledEventObject.addProperty("enabled", enable);
264 invoke(HttpMethod.PUT, uri, null, jsonObject.toString());
268 * Invoke a call on the hub server to retrieve information or send a command
270 * @param method GET or PUT
271 * @param url the host url to be called
272 * @param query the http query parameter
273 * @param jsonCommand the request command content (as a json string)
274 * @return the response content (as a json string)
275 * @throws HubMaintenanceException
276 * @throws HubProcessingException
278 private synchronized String invoke(HttpMethod method, String url, @Nullable Query query,
279 @Nullable String jsonCommand) throws HubMaintenanceException, HubProcessingException {
280 if (logger.isTraceEnabled()) {
282 logger.trace("API command {} {}{}", method, url, query);
284 logger.trace("API command {} {}", method, url);
286 if (jsonCommand != null) {
287 logger.trace("JSON command = {}", jsonCommand);
290 Request request = httpClient.newRequest(url).method(method).header("Connection", "close").accept("*/*");
292 request.param(query.getKey(), query.getValue());
294 if (jsonCommand != null) {
295 request.header(HttpHeader.CONTENT_TYPE, "application/json").content(new StringContentProvider(jsonCommand));
297 ContentResponse response;
299 response = request.send();
300 } catch (InterruptedException | TimeoutException | ExecutionException e) {
301 if (Instant.now().isBefore(maintenanceScheduledEnd)) {
302 // throw "softer" exception during maintenance window
303 logger.debug("Hub still undergoing maintenance");
304 throw new HubMaintenanceException("Hub still undergoing maintenance");
306 throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
308 int statusCode = response.getStatus();
309 if (statusCode == HttpStatus.LOCKED_423) {
310 // set end of maintenance window, and throw a "softer" exception
311 maintenanceScheduledEnd = Instant.now().plusSeconds(maintenancePeriod);
312 logger.debug("Hub undergoing maintenance");
313 throw new HubMaintenanceException("Hub undergoing maintenance");
315 if (statusCode != HttpStatus.OK_200) {
316 logger.warn("Hub returned HTTP {} {}", statusCode, response.getReason());
317 throw new HubProcessingException(String.format("HTTP %d error", statusCode));
319 String jsonResponse = response.getContentAsString();
320 if ("".equals(jsonResponse)) {
321 logger.warn("Hub returned no content");
322 throw new HubProcessingException("Missing response entity");
324 if (logger.isTraceEnabled()) {
325 logger.trace("JSON response = {}", jsonResponse);
331 * Fetches a JSON package that describes a specific shade in the hub, and wraps it
332 * in a Shade class instance
334 * @param shadeId id of the shade to be fetched
335 * @return Shade class instance
336 * @throws JsonParseException if there is a JSON parsing error
337 * @throws HubProcessingException if there is any processing error
338 * @throws HubMaintenanceException if the hub is down for maintenance
340 public @Nullable Shade getShade(int shadeId)
341 throws JsonParseException, HubProcessingException, HubMaintenanceException {
342 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), null, null);
343 return gson.fromJson(json, Shade.class);
347 * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
348 * a specific shade's position; fetches a JSON package that describes that shade,
349 * and wraps it in a Shade class instance
351 * @param shadeId id of the shade to be refreshed
352 * @return Shade class instance
353 * @throws JsonParseException if there is a JSON parsing error
354 * @throws HubProcessingException if there is any processing error
355 * @throws HubMaintenanceException if the hub is down for maintenance
357 public @Nullable Shade refreshShadePosition(int shadeId)
358 throws JsonParseException, HubProcessingException, HubMaintenanceException {
359 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
360 Query.of("refresh", Boolean.toString(true)), null);
361 return gson.fromJson(json, Shade.class);
365 * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
366 * a specific shade's survey data, which will also refresh signal strength;
367 * fetches a JSON package that describes that survey, and wraps it in a Survey
370 * @param shadeId id of the shade to be surveyed
371 * @return Survey class instance
372 * @throws JsonParseException if there is a JSON parsing error
373 * @throws HubProcessingException if there is any processing error
374 * @throws HubMaintenanceException if the hub is down for maintenance
376 public @Nullable Survey getShadeSurvey(int shadeId)
377 throws JsonParseException, HubProcessingException, HubMaintenanceException {
378 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
379 Query.of("survey", Boolean.toString(true)), null);
380 return gson.fromJson(json, Survey.class);
384 * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
385 * a specific shade's battery level; fetches a JSON package that describes that shade,
386 * and wraps it in a Shade class instance
388 * @param shadeId id of the shade to be refreshed
389 * @return Shade class instance
390 * @throws JsonParseException if there is a JSON parsing error
391 * @throws HubProcessingException if there is any processing error
392 * @throws HubMaintenanceException if the hub is down for maintenance
394 public @Nullable Shade refreshShadeBatteryLevel(int shadeId)
395 throws JsonParseException, HubProcessingException, HubMaintenanceException {
396 String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
397 Query.of("updateBatteryLevel", Boolean.toString(true)), null);
398 return gson.fromJson(json, Shade.class);
402 * Tells the hub to stop movement of a specific shade
404 * @param shadeId id of the shade to be stopped
405 * @throws HubProcessingException if there is any processing error
406 * @throws HubMaintenanceException if the hub is down for maintenance
408 public void stopShade(int shadeId) throws HubProcessingException, HubMaintenanceException {
409 String json = gson.toJson(new ShadeStop(shadeId));
410 invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json);