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