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