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