]> git.basschouten.com Git - openhab-addons.git/blob
4ce607b8dbe36ee4069062c170d86dab6d7db806
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.hdpowerview.internal;
14
15 import java.time.Duration;
16 import java.time.Instant;
17 import java.util.List;
18 import java.util.concurrent.ExecutionException;
19 import java.util.concurrent.TimeoutException;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.eclipse.jetty.client.HttpClient;
24 import org.eclipse.jetty.client.api.ContentResponse;
25 import org.eclipse.jetty.client.api.Request;
26 import org.eclipse.jetty.client.util.StringContentProvider;
27 import org.eclipse.jetty.http.HttpHeader;
28 import org.eclipse.jetty.http.HttpMethod;
29 import org.eclipse.jetty.http.HttpStatus;
30 import org.openhab.binding.hdpowerview.internal.dto.Color;
31 import org.openhab.binding.hdpowerview.internal.dto.HubFirmware;
32 import org.openhab.binding.hdpowerview.internal.dto.Scene;
33 import org.openhab.binding.hdpowerview.internal.dto.SceneCollection;
34 import org.openhab.binding.hdpowerview.internal.dto.ScheduledEvent;
35 import org.openhab.binding.hdpowerview.internal.dto.ShadeData;
36 import org.openhab.binding.hdpowerview.internal.dto.ShadePosition;
37 import org.openhab.binding.hdpowerview.internal.dto.SurveyData;
38 import org.openhab.binding.hdpowerview.internal.dto.UserData;
39 import org.openhab.binding.hdpowerview.internal.dto.requests.RepeaterBlinking;
40 import org.openhab.binding.hdpowerview.internal.dto.requests.RepeaterColor;
41 import org.openhab.binding.hdpowerview.internal.dto.requests.ShadeCalibrate;
42 import org.openhab.binding.hdpowerview.internal.dto.requests.ShadeJog;
43 import org.openhab.binding.hdpowerview.internal.dto.requests.ShadeMove;
44 import org.openhab.binding.hdpowerview.internal.dto.requests.ShadeStop;
45 import org.openhab.binding.hdpowerview.internal.dto.responses.FirmwareVersion;
46 import org.openhab.binding.hdpowerview.internal.dto.responses.Repeater;
47 import org.openhab.binding.hdpowerview.internal.dto.responses.RepeaterData;
48 import org.openhab.binding.hdpowerview.internal.dto.responses.Repeaters;
49 import org.openhab.binding.hdpowerview.internal.dto.responses.SceneCollections;
50 import org.openhab.binding.hdpowerview.internal.dto.responses.Scenes;
51 import org.openhab.binding.hdpowerview.internal.dto.responses.ScheduledEvents;
52 import org.openhab.binding.hdpowerview.internal.dto.responses.Shade;
53 import org.openhab.binding.hdpowerview.internal.dto.responses.Shades;
54 import org.openhab.binding.hdpowerview.internal.dto.responses.Survey;
55 import org.openhab.binding.hdpowerview.internal.dto.responses.UserDataResponse;
56 import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException;
57 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
58 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
59 import org.openhab.binding.hdpowerview.internal.exceptions.HubShadeTimeoutException;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63 import com.google.gson.Gson;
64 import com.google.gson.JsonElement;
65 import com.google.gson.JsonObject;
66 import com.google.gson.JsonParseException;
67 import com.google.gson.JsonParser;
68
69 /**
70  * JAX-RS targets for communicating with an HD PowerView hub
71  *
72  * @author Andy Lintner - Initial contribution
73  * @author Andrew Fiddian-Green - Added support for secondary rail positions
74  * @author Jacob Laursen - Added support for scene groups and automations
75  */
76 @NonNullByDefault
77 public class HDPowerViewWebTargets {
78
79     private final Logger logger = LoggerFactory.getLogger(HDPowerViewWebTargets.class);
80
81     /*
82      * the hub returns a 423 error (resource locked) daily just after midnight;
83      * which means it is temporarily undergoing maintenance; so we use "soft"
84      * exception handling during the five minute maintenance period after a 423
85      * error is received
86      */
87     private final Duration maintenancePeriod = Duration.ofMinutes(5);
88     private Instant maintenanceScheduledEnd = Instant.MIN;
89
90     private final String base;
91     private final String firmwareVersion;
92     private final String shades;
93     private final String sceneActivate;
94     private final String scenes;
95     private final String sceneCollectionActivate;
96     private final String sceneCollections;
97     private final String scheduledEvents;
98     private final String repeaters;
99     private final String userData;
100
101     private final Gson gson = new Gson();
102     private final HttpClient httpClient;
103
104     /**
105      * private helper class for passing http url query parameters
106      */
107     private static class Query {
108         private final String key;
109         private final String value;
110
111         private Query(String key, String value) {
112             this.key = key;
113             this.value = value;
114         }
115
116         public static Query of(String key, String value) {
117             return new Query(key, value);
118         }
119
120         public String getKey() {
121             return key;
122         }
123
124         public String getValue() {
125             return value;
126         }
127
128         @Override
129         public String toString() {
130             return String.format("?%s=%s", key, value);
131         }
132     }
133
134     /**
135      * Initialize the web targets
136      *
137      * @param httpClient the HTTP client (the binding)
138      * @param ipAddress the IP address of the server (the hub)
139      */
140     public HDPowerViewWebTargets(HttpClient httpClient, String ipAddress) {
141         base = "http://" + ipAddress + "/api/";
142         shades = base + "shades/";
143         firmwareVersion = base + "fwversion";
144         sceneActivate = base + "scenes";
145         scenes = base + "scenes/";
146
147         // Hub v1 only supports "scenecollections". Hub v2 will redirect to "sceneCollections".
148         sceneCollectionActivate = base + "scenecollections";
149         sceneCollections = base + "scenecollections/";
150
151         scheduledEvents = base + "scheduledevents";
152         repeaters = base + "repeaters/";
153         userData = base + "userdata";
154
155         this.httpClient = httpClient;
156     }
157
158     /**
159      * Fetches a JSON package with firmware information for the hub.
160      *
161      * @return FirmwareVersions class instance
162      * @throws HubInvalidResponseException if response is invalid
163      * @throws HubProcessingException if there is any processing error
164      * @throws HubMaintenanceException if the hub is down for maintenance
165      */
166     public HubFirmware getFirmwareVersions()
167             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
168         String json = invoke(HttpMethod.GET, firmwareVersion, null, null);
169         try {
170             FirmwareVersion firmwareVersion = gson.fromJson(json, FirmwareVersion.class);
171             if (firmwareVersion == null) {
172                 throw new HubInvalidResponseException("Missing firmware response");
173             }
174             HubFirmware firmwareVersions = firmwareVersion.firmware;
175             if (firmwareVersions == null) {
176                 throw new HubInvalidResponseException("Missing 'firmware' element");
177             }
178             return firmwareVersions;
179         } catch (JsonParseException e) {
180             throw new HubInvalidResponseException("Error parsing firmware response", e);
181         }
182     }
183
184     /**
185      * Fetches a JSON package with user data information for the hub.
186      *
187      * @return {@link UserData} class instance
188      * @throws HubInvalidResponseException if response is invalid
189      * @throws HubProcessingException if there is any processing error
190      * @throws HubMaintenanceException if the hub is down for maintenance
191      */
192     public UserData getUserData() throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
193         String json = invoke(HttpMethod.GET, userData, null, null);
194         try {
195             UserDataResponse userDataResponse = gson.fromJson(json, UserDataResponse.class);
196             if (userDataResponse == null) {
197                 throw new HubInvalidResponseException("Missing userData response");
198             }
199             UserData userData = userDataResponse.userData;
200             if (userData == null) {
201                 throw new HubInvalidResponseException("Missing 'userData' element");
202             }
203             return userData;
204         } catch (JsonParseException e) {
205             throw new HubInvalidResponseException("Error parsing userData response", e);
206         }
207     }
208
209     /**
210      * Fetches a JSON package that describes all shades in the hub, and wraps it in
211      * a Shades class instance
212      *
213      * @return Shades class instance
214      * @throws HubInvalidResponseException if response is invalid
215      * @throws HubProcessingException if there is any processing error
216      * @throws HubMaintenanceException if the hub is down for maintenance
217      */
218     public Shades getShades() throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
219         String json = invoke(HttpMethod.GET, shades, null, null);
220         try {
221             Shades shades = gson.fromJson(json, Shades.class);
222             if (shades == null) {
223                 throw new HubInvalidResponseException("Missing shades response");
224             }
225             List<ShadeData> shadeData = shades.shadeData;
226             if (shadeData == null) {
227                 throw new HubInvalidResponseException("Missing 'shades.shadeData' element");
228             }
229             return shades;
230         } catch (JsonParseException e) {
231             throw new HubInvalidResponseException("Error parsing shades response", e);
232         }
233     }
234
235     /**
236      * Instructs the hub to move a specific shade
237      *
238      * @param shadeId id of the shade to be moved
239      * @param position instance of ShadePosition containing the new position
240      * @return ShadeData class instance (with new position)
241      * @throws HubInvalidResponseException if response is invalid
242      * @throws HubProcessingException if there is any processing error
243      * @throws HubMaintenanceException if the hub is down for maintenance
244      * @throws HubShadeTimeoutException if the shade did not respond to a request
245      */
246     public ShadeData moveShade(int shadeId, ShadePosition position) throws HubInvalidResponseException,
247             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
248         String jsonRequest = gson.toJson(new ShadeMove(position));
249         String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest);
250         return shadeDataFromJson(jsonResponse);
251     }
252
253     private ShadeData shadeDataFromJson(String json) throws HubInvalidResponseException, HubShadeTimeoutException {
254         try {
255             Shade shade = gson.fromJson(json, Shade.class);
256             if (shade == null) {
257                 throw new HubInvalidResponseException("Missing shade response");
258             }
259             ShadeData shadeData = shade.shade;
260             if (shadeData == null) {
261                 throw new HubInvalidResponseException("Missing 'shade.shade' element");
262             }
263             if (Boolean.TRUE.equals(shadeData.timedOut)) {
264                 throw new HubShadeTimeoutException("Timeout when sending request to the shade");
265             }
266             return shadeData;
267         } catch (JsonParseException e) {
268             throw new HubInvalidResponseException("Error parsing shade response", e);
269         }
270     }
271
272     /**
273      * Instructs the hub to stop movement of a specific shade
274      *
275      * @param shadeId id of the shade to be stopped
276      * @return ShadeData class instance (new position cannot be relied upon)
277      * @throws HubInvalidResponseException if response is invalid
278      * @throws HubProcessingException if there is any processing error
279      * @throws HubMaintenanceException if the hub is down for maintenance
280      * @throws HubShadeTimeoutException if the shade did not respond to a request
281      */
282     public ShadeData stopShade(int shadeId) throws HubInvalidResponseException, HubProcessingException,
283             HubMaintenanceException, HubShadeTimeoutException {
284         String jsonRequest = gson.toJson(new ShadeStop());
285         String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest);
286         return shadeDataFromJson(jsonResponse);
287     }
288
289     /**
290      * Instructs the hub to jog a specific shade
291      *
292      * @param shadeId id of the shade to be jogged
293      * @return ShadeData class instance
294      * @throws HubInvalidResponseException if response is invalid
295      * @throws HubProcessingException if there is any processing error
296      * @throws HubMaintenanceException if the hub is down for maintenance
297      * @throws HubShadeTimeoutException if the shade did not respond to a request
298      */
299     public ShadeData jogShade(int shadeId) throws HubInvalidResponseException, HubProcessingException,
300             HubMaintenanceException, HubShadeTimeoutException {
301         String jsonRequest = gson.toJson(new ShadeJog());
302         String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest);
303         return shadeDataFromJson(jsonResponse);
304     }
305
306     /**
307      * Instructs the hub to calibrate a specific shade
308      *
309      * @param shadeId id of the shade to be calibrated
310      * @return ShadeData class instance
311      * @throws HubInvalidResponseException if response is invalid
312      * @throws HubProcessingException if there is any processing error
313      * @throws HubMaintenanceException if the hub is down for maintenance
314      * @throws HubShadeTimeoutException if the shade did not respond to a request
315      */
316     public ShadeData calibrateShade(int shadeId) throws HubInvalidResponseException, HubProcessingException,
317             HubMaintenanceException, HubShadeTimeoutException {
318         String jsonRequest = gson.toJson(new ShadeCalibrate());
319         String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest);
320         return shadeDataFromJson(jsonResponse);
321     }
322
323     /**
324      * Fetches a JSON package that describes all scenes in the hub, and wraps it in
325      * a Scenes class instance
326      *
327      * @return Scenes class instance
328      * @throws HubInvalidResponseException if response is invalid
329      * @throws HubProcessingException if there is any processing error
330      * @throws HubMaintenanceException if the hub is down for maintenance
331      */
332     public Scenes getScenes() throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
333         String json = invoke(HttpMethod.GET, scenes, null, null);
334         try {
335             Scenes scenes = gson.fromJson(json, Scenes.class);
336             if (scenes == null) {
337                 throw new HubInvalidResponseException("Missing scenes response");
338             }
339             List<Scene> sceneData = scenes.sceneData;
340             if (sceneData == null) {
341                 throw new HubInvalidResponseException("Missing 'scenes.sceneData' element");
342             }
343             return scenes;
344         } catch (JsonParseException e) {
345             throw new HubInvalidResponseException("Error parsing scenes response", e);
346         }
347     }
348
349     /**
350      * Instructs the hub to execute a specific scene
351      *
352      * @param sceneId id of the scene to be executed
353      * @throws HubProcessingException if there is any processing error
354      * @throws HubMaintenanceException if the hub is down for maintenance
355      */
356     public void activateScene(int sceneId) throws HubProcessingException, HubMaintenanceException {
357         invoke(HttpMethod.GET, sceneActivate, Query.of("sceneId", Integer.toString(sceneId)), null);
358     }
359
360     /**
361      * Fetches a JSON package that describes all scene collections in the hub, and wraps it in
362      * a SceneCollections class instance
363      *
364      * @return SceneCollections class instance
365      * @throws HubInvalidResponseException if response is invalid
366      * @throws HubProcessingException if there is any processing error
367      * @throws HubMaintenanceException if the hub is down for maintenance
368      */
369     public SceneCollections getSceneCollections()
370             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
371         String json = invoke(HttpMethod.GET, sceneCollections, null, null);
372         try {
373             SceneCollections sceneCollections = gson.fromJson(json, SceneCollections.class);
374             if (sceneCollections == null) {
375                 throw new HubInvalidResponseException("Missing sceneCollections response");
376             }
377             List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
378             if (sceneCollectionData == null) {
379                 throw new HubInvalidResponseException("Missing 'sceneCollections.sceneCollectionData' element");
380             }
381             return sceneCollections;
382         } catch (JsonParseException e) {
383             throw new HubInvalidResponseException("Error parsing sceneCollections response", e);
384         }
385     }
386
387     /**
388      * Instructs the hub to execute a specific scene collection
389      *
390      * @param sceneCollectionId id of the scene collection to be executed
391      * @throws HubProcessingException if there is any processing error
392      * @throws HubMaintenanceException if the hub is down for maintenance
393      */
394     public void activateSceneCollection(int sceneCollectionId) throws HubProcessingException, HubMaintenanceException {
395         invoke(HttpMethod.GET, sceneCollectionActivate,
396                 Query.of("sceneCollectionId", Integer.toString(sceneCollectionId)), null);
397     }
398
399     /**
400      * Fetches a JSON package that describes all scheduled events in the hub, and wraps it in
401      * a ScheduledEvents class instance
402      *
403      * @return ScheduledEvents class instance
404      * @throws HubInvalidResponseException if response is invalid
405      * @throws HubProcessingException if there is any processing error
406      * @throws HubMaintenanceException if the hub is down for maintenance
407      */
408     public ScheduledEvents getScheduledEvents()
409             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
410         String json = invoke(HttpMethod.GET, scheduledEvents, null, null);
411         try {
412             ScheduledEvents scheduledEvents = gson.fromJson(json, ScheduledEvents.class);
413             if (scheduledEvents == null) {
414                 throw new HubInvalidResponseException("Missing scheduledEvents response");
415             }
416             List<ScheduledEvent> scheduledEventData = scheduledEvents.scheduledEventData;
417             if (scheduledEventData == null) {
418                 throw new HubInvalidResponseException("Missing 'scheduledEvents.scheduledEventData' element");
419             }
420             return scheduledEvents;
421         } catch (JsonParseException e) {
422             throw new HubInvalidResponseException("Error parsing scheduledEvents response", e);
423         }
424     }
425
426     /**
427      * Enables or disables a scheduled event in the hub.
428      * 
429      * @param scheduledEventId id of the scheduled event to be enabled or disabled
430      * @param enable true to enable scheduled event, false to disable
431      * @throws HubInvalidResponseException if response is invalid
432      * @throws HubProcessingException if there is any processing error
433      * @throws HubMaintenanceException if the hub is down for maintenance
434      */
435     public void enableScheduledEvent(int scheduledEventId, boolean enable)
436             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
437         String uri = scheduledEvents + "/" + scheduledEventId;
438         String jsonResponse = invoke(HttpMethod.GET, uri, null, null);
439         try {
440             JsonObject jsonObject = JsonParser.parseString(jsonResponse).getAsJsonObject();
441             JsonElement scheduledEventElement = jsonObject.get("scheduledEvent");
442             if (scheduledEventElement == null) {
443                 throw new HubInvalidResponseException("Missing 'scheduledEvent' element");
444             }
445             JsonObject scheduledEventObject = scheduledEventElement.getAsJsonObject();
446             scheduledEventObject.addProperty("enabled", enable);
447             invoke(HttpMethod.PUT, uri, null, jsonObject.toString());
448         } catch (JsonParseException | IllegalStateException e) {
449             throw new HubInvalidResponseException("Error parsing scheduledEvent response", e);
450         }
451     }
452
453     /**
454      * Fetches a JSON package that describes all repeaters in the hub, and wraps it in
455      * a Repeaters class instance
456      *
457      * @return Repeaters class instance
458      * @throws HubInvalidResponseException if response is invalid
459      * @throws HubProcessingException if there is any processing error
460      * @throws HubMaintenanceException if the hub is down for maintenance
461      */
462     public Repeaters getRepeaters()
463             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
464         String json = invoke(HttpMethod.GET, repeaters, null, null);
465         try {
466             Repeaters repeaters = gson.fromJson(json, Repeaters.class);
467             if (repeaters == null) {
468                 throw new HubInvalidResponseException("Missing repeaters response");
469             }
470             List<RepeaterData> repeaterData = repeaters.repeaterData;
471             if (repeaterData == null) {
472                 throw new HubInvalidResponseException("Missing 'repeaters.repeaterData' element");
473             }
474             return repeaters;
475         } catch (JsonParseException e) {
476             throw new HubInvalidResponseException("Error parsing repeaters response", e);
477         }
478     }
479
480     /**
481      * Fetches a JSON package that describes a specific repeater in the hub, and wraps it
482      * in a RepeaterData class instance
483      *
484      * @param repeaterId id of the repeater to be fetched
485      * @return RepeaterData class instance
486      * @throws HubInvalidResponseException if response is invalid
487      * @throws HubProcessingException if there is any processing error
488      * @throws HubMaintenanceException if the hub is down for maintenance
489      */
490     public RepeaterData getRepeater(int repeaterId)
491             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
492         String jsonResponse = invoke(HttpMethod.GET, repeaters + Integer.toString(repeaterId), null, null);
493         return repeaterDataFromJson(jsonResponse);
494     }
495
496     private RepeaterData repeaterDataFromJson(String json) throws HubInvalidResponseException {
497         try {
498             Repeater repeater = gson.fromJson(json, Repeater.class);
499             if (repeater == null) {
500                 throw new HubInvalidResponseException("Missing repeater response");
501             }
502             RepeaterData repeaterData = repeater.repeater;
503             if (repeaterData == null) {
504                 throw new HubInvalidResponseException("Missing 'repeater.repeater' element");
505             }
506             return repeaterData;
507         } catch (JsonParseException e) {
508             throw new HubInvalidResponseException("Error parsing repeater response", e);
509         }
510     }
511
512     /**
513      * Instructs the hub to identify a specific repeater by blinking
514      *
515      * @param repeaterId id of the repeater to be identified
516      * @return RepeaterData class instance
517      * @throws HubInvalidResponseException if response is invalid
518      * @throws HubProcessingException if there is any processing error
519      * @throws HubMaintenanceException if the hub is down for maintenance
520      */
521     public RepeaterData identifyRepeater(int repeaterId)
522             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
523         String jsonResponse = invoke(HttpMethod.GET, repeaters + repeaterId,
524                 Query.of("identify", Boolean.toString(true)), null);
525         return repeaterDataFromJson(jsonResponse);
526     }
527
528     /**
529      * Enables or disables blinking for a repeater
530      * 
531      * @param repeaterId id of the repeater for which to be enable or disable blinking
532      * @param enable true to enable blinking, false to disable
533      * @return RepeaterData class instance
534      * @throws HubInvalidResponseException if response is invalid
535      * @throws HubProcessingException if there is any processing error
536      * @throws HubMaintenanceException if the hub is down for maintenance
537      */
538     public RepeaterData enableRepeaterBlinking(int repeaterId, boolean enable)
539             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
540         String jsonRequest = gson.toJson(new RepeaterBlinking(repeaterId, enable));
541         String jsonResponse = invoke(HttpMethod.PUT, repeaters + repeaterId, null, jsonRequest);
542         return repeaterDataFromJson(jsonResponse);
543     }
544
545     /**
546      * Sets color and brightness for a repeater
547      *
548      * @param repeaterId id of the repeater for which to set color and brightness
549      * @return RepeaterData class instance
550      * @throws HubInvalidResponseException if response is invalid
551      * @throws HubProcessingException if there is any processing error
552      * @throws HubMaintenanceException if the hub is down for maintenance
553      */
554     public RepeaterData setRepeaterColor(int repeaterId, Color color)
555             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
556         String jsonRequest = gson.toJson(new RepeaterColor(repeaterId, color));
557         String jsonResponse = invoke(HttpMethod.PUT, repeaters + repeaterId, null, jsonRequest);
558         return repeaterDataFromJson(jsonResponse);
559     }
560
561     /**
562      * Invoke a call on the hub server to retrieve information or send a command
563      *
564      * @param method GET or PUT
565      * @param url the host url to be called
566      * @param query the http query parameter
567      * @param jsonCommand the request command content (as a json string)
568      * @return the response content (as a json string)
569      * @throws HubMaintenanceException
570      * @throws HubProcessingException
571      */
572     private synchronized String invoke(HttpMethod method, String url, @Nullable Query query,
573             @Nullable String jsonCommand) throws HubMaintenanceException, HubProcessingException {
574         if (logger.isTraceEnabled()) {
575             if (query != null) {
576                 logger.trace("API command {} {}{}", method, url, query);
577             } else {
578                 logger.trace("API command {} {}", method, url);
579             }
580             if (jsonCommand != null) {
581                 logger.trace("JSON command = {}", jsonCommand);
582             }
583         }
584         Request request = httpClient.newRequest(url).method(method).header("Connection", "close").accept("*/*");
585         if (query != null) {
586             request.param(query.getKey(), query.getValue());
587         }
588         if (jsonCommand != null) {
589             request.header(HttpHeader.CONTENT_TYPE, "application/json").content(new StringContentProvider(jsonCommand));
590         }
591         ContentResponse response;
592         try {
593             response = request.send();
594         } catch (InterruptedException e) {
595             Thread.currentThread().interrupt();
596             throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
597         } catch (TimeoutException | ExecutionException e) {
598             if (Instant.now().isBefore(maintenanceScheduledEnd)) {
599                 // throw "softer" exception during maintenance window
600                 logger.debug("Hub still undergoing maintenance");
601                 throw new HubMaintenanceException("Hub still undergoing maintenance");
602             }
603             throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
604         }
605         int statusCode = response.getStatus();
606         if (statusCode == HttpStatus.LOCKED_423) {
607             // set end of maintenance window, and throw a "softer" exception
608             maintenanceScheduledEnd = Instant.now().plus(maintenancePeriod);
609             logger.debug("Hub undergoing maintenance");
610             throw new HubMaintenanceException("Hub undergoing maintenance");
611         }
612         if (statusCode != HttpStatus.OK_200) {
613             logger.warn("Hub returned HTTP {} {}", statusCode, response.getReason());
614             throw new HubProcessingException(String.format("HTTP %d error", statusCode));
615         }
616         String jsonResponse = response.getContentAsString();
617         if (logger.isTraceEnabled()) {
618             logger.trace("JSON response = {}", jsonResponse);
619         }
620         if (jsonResponse == null || jsonResponse.isEmpty()) {
621             logger.warn("Hub returned no content");
622             throw new HubProcessingException("Missing response entity");
623         }
624         return jsonResponse;
625     }
626
627     /**
628      * Fetches a JSON package that describes a specific shade in the hub, and wraps it
629      * in a Shade class instance
630      *
631      * @param shadeId id of the shade to be fetched
632      * @return ShadeData class instance
633      * @throws HubInvalidResponseException if response is invalid
634      * @throws HubProcessingException if there is any processing error
635      * @throws HubMaintenanceException if the hub is down for maintenance
636      * @throws HubShadeTimeoutException if the shade did not respond to a request
637      */
638     public ShadeData getShade(int shadeId) throws HubInvalidResponseException, HubProcessingException,
639             HubMaintenanceException, HubShadeTimeoutException {
640         String jsonResponse = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), null, null);
641         return shadeDataFromJson(jsonResponse);
642     }
643
644     /**
645      * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
646      * a specific shade's position; fetches a JSON package that describes that shade,
647      * and wraps it in a Shade class instance
648      *
649      * @param shadeId id of the shade to be refreshed
650      * @return ShadeData class instance
651      * @throws HubInvalidResponseException if response is invalid
652      * @throws HubProcessingException if there is any processing error
653      * @throws HubMaintenanceException if the hub is down for maintenance
654      * @throws HubShadeTimeoutException if the shade did not respond to a request
655      */
656     public ShadeData refreshShadePosition(int shadeId)
657             throws JsonParseException, HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
658         String jsonResponse = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
659                 Query.of("refresh", Boolean.toString(true)), null);
660         return shadeDataFromJson(jsonResponse);
661     }
662
663     /**
664      * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
665      * a specific shade's survey data, which will also refresh signal strength;
666      * fetches a JSON package that describes that survey, and wraps it in a Survey
667      * class instance
668      *
669      * @param shadeId id of the shade to be surveyed
670      * @return List of SurveyData class instances
671      * @throws HubInvalidResponseException if response is invalid
672      * @throws HubProcessingException if there is any processing error
673      * @throws HubMaintenanceException if the hub is down for maintenance
674      */
675     public List<SurveyData> getShadeSurvey(int shadeId)
676             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
677         String jsonResponse = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
678                 Query.of("survey", Boolean.toString(true)), null);
679         try {
680             Survey survey = gson.fromJson(jsonResponse, Survey.class);
681             if (survey == null) {
682                 throw new HubInvalidResponseException("Missing survey response");
683             }
684             List<SurveyData> surveyData = survey.surveyData;
685             if (surveyData == null) {
686                 throw new HubInvalidResponseException("Missing 'survey.surveyData' element");
687             }
688             return surveyData;
689         } catch (JsonParseException e) {
690             throw new HubInvalidResponseException("Error parsing survey response", e);
691         }
692     }
693
694     /**
695      * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
696      * a specific shade's battery level; fetches a JSON package that describes that shade,
697      * and wraps it in a Shade class instance
698      *
699      * @param shadeId id of the shade to be refreshed
700      * @return ShadeData class instance
701      * @throws HubInvalidResponseException if response is invalid
702      * @throws HubProcessingException if there is any processing error
703      * @throws HubMaintenanceException if the hub is down for maintenance
704      * @throws HubShadeTimeoutException if the shade did not respond to a request
705      */
706     public ShadeData refreshShadeBatteryLevel(int shadeId) throws HubInvalidResponseException, HubProcessingException,
707             HubMaintenanceException, HubShadeTimeoutException {
708         String jsonResponse = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
709                 Query.of("updateBatteryLevel", Boolean.toString(true)), null);
710         return shadeDataFromJson(jsonResponse);
711     }
712 }