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