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