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