2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.hdpowerview.internal;
15 import java.time.Instant;
16 import java.util.List;
17 import java.util.concurrent.ExecutionException;
18 import java.util.concurrent.TimeoutException;
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;
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;
63 * JAX-RS targets for communicating with an HD PowerView hub
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
70 public class HDPowerViewWebTargets {
72 private final Logger logger = LoggerFactory.getLogger(HDPowerViewWebTargets.class);
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
80 private final int maintenancePeriod = 300;
81 private Instant maintenanceScheduledEnd = Instant.now().minusSeconds(2 * maintenancePeriod);
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;
93 private final Gson gson = new Gson();
94 private final HttpClient httpClient;
97 * private helper class for passing http url query parameters
99 private static class Query {
100 private final String key;
101 private final String value;
103 private Query(String key, String value) {
108 public static Query of(String key, String value) {
109 return new Query(key, value);
112 public String getKey() {
116 public String getValue() {
121 public String toString() {
122 return String.format("?%s=%s", key, value);
127 * Initialize the web targets
129 * @param httpClient the HTTP client (the binding)
130 * @param ipAddress the IP address of the server (the hub)
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/";
139 // Hub v1 only supports "scenecollections". Hub v2 will redirect to "sceneCollections".
140 sceneCollectionActivate = base + "scenecollections";
141 sceneCollections = base + "scenecollections/";
143 scheduledEvents = base + "scheduledevents";
145 repeaters = base + "repeaters/";
147 this.httpClient = httpClient;
151 * Fetches a JSON package with firmware information for the hub.
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
158 public FirmwareVersions getFirmwareVersions()
159 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
160 String json = invoke(HttpMethod.GET, firmwareVersion, null, null);
162 FirmwareVersion firmwareVersion = gson.fromJson(json, FirmwareVersion.class);
163 if (firmwareVersion == null) {
164 throw new HubInvalidResponseException("Missing firmware response");
166 FirmwareVersions firmwareVersions = firmwareVersion.firmware;
167 if (firmwareVersions == null) {
168 throw new HubInvalidResponseException("Missing 'firmware' element");
170 return firmwareVersions;
171 } catch (JsonParseException e) {
172 throw new HubInvalidResponseException("Error parsing firmware response", e);
177 * Fetches a JSON package that describes all shades in the hub, and wraps it in
178 * a Shades class instance
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
185 public Shades getShades() throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
186 String json = invoke(HttpMethod.GET, shades, null, null);
188 Shades shades = gson.fromJson(json, Shades.class);
189 if (shades == null) {
190 throw new HubInvalidResponseException("Missing shades response");
192 List<ShadeData> shadeData = shades.shadeData;
193 if (shadeData == null) {
194 throw new HubInvalidResponseException("Missing 'shades.shadeData' element");
197 } catch (JsonParseException e) {
198 throw new HubInvalidResponseException("Error parsing shades response", e);
203 * Instructs the hub to move a specific shade
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
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);
219 private ShadeData shadeDataFromJson(String json) throws HubInvalidResponseException {
221 Shade shade = gson.fromJson(json, Shade.class);
223 throw new HubInvalidResponseException("Missing shade response");
225 ShadeData shadeData = shade.shade;
226 if (shadeData == null) {
227 throw new HubInvalidResponseException("Missing 'shade.shade' element");
230 } catch (JsonParseException e) {
231 throw new HubInvalidResponseException("Error parsing shade response", e);
236 * Instructs the hub to stop movement of a specific shade
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
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);
252 * Instructs the hub to jog a specific shade
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
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);
268 * Instructs the hub to calibrate a specific shade
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
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);
284 * Fetches a JSON package that describes all scenes in the hub, and wraps it in
285 * a Scenes class instance
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
292 public Scenes getScenes() throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
293 String json = invoke(HttpMethod.GET, scenes, null, null);
295 Scenes scenes = gson.fromJson(json, Scenes.class);
296 if (scenes == null) {
297 throw new HubInvalidResponseException("Missing scenes response");
299 List<Scene> sceneData = scenes.sceneData;
300 if (sceneData == null) {
301 throw new HubInvalidResponseException("Missing 'scenes.sceneData' element");
304 } catch (JsonParseException e) {
305 throw new HubInvalidResponseException("Error parsing scenes response", e);
310 * Instructs the hub to execute a specific scene
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
316 public void activateScene(int sceneId) throws HubProcessingException, HubMaintenanceException {
317 invoke(HttpMethod.GET, sceneActivate, Query.of("sceneId", Integer.toString(sceneId)), null);
321 * Fetches a JSON package that describes all scene collections in the hub, and wraps it in
322 * a SceneCollections class instance
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
329 public SceneCollections getSceneCollections()
330 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
331 String json = invoke(HttpMethod.GET, sceneCollections, null, null);
333 SceneCollections sceneCollections = gson.fromJson(json, SceneCollections.class);
334 if (sceneCollections == null) {
335 throw new HubInvalidResponseException("Missing sceneCollections response");
337 List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
338 if (sceneCollectionData == null) {
339 throw new HubInvalidResponseException("Missing 'sceneCollections.sceneCollectionData' element");
341 return sceneCollections;
342 } catch (JsonParseException e) {
343 throw new HubInvalidResponseException("Error parsing sceneCollections response", e);
348 * Instructs the hub to execute a specific scene collection
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
354 public void activateSceneCollection(int sceneCollectionId) throws HubProcessingException, HubMaintenanceException {
355 invoke(HttpMethod.GET, sceneCollectionActivate,
356 Query.of("sceneCollectionId", Integer.toString(sceneCollectionId)), null);
360 * Fetches a JSON package that describes all scheduled events in the hub, and wraps it in
361 * a ScheduledEvents class instance
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
368 public ScheduledEvents getScheduledEvents()
369 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
370 String json = invoke(HttpMethod.GET, scheduledEvents, null, null);
372 ScheduledEvents scheduledEvents = gson.fromJson(json, ScheduledEvents.class);
373 if (scheduledEvents == null) {
374 throw new HubInvalidResponseException("Missing scheduledEvents response");
376 List<ScheduledEvent> scheduledEventData = scheduledEvents.scheduledEventData;
377 if (scheduledEventData == null) {
378 throw new HubInvalidResponseException("Missing 'scheduledEvents.scheduledEventData' element");
380 return scheduledEvents;
381 } catch (JsonParseException e) {
382 throw new HubInvalidResponseException("Error parsing scheduledEvents response", e);
387 * Enables or disables a scheduled event in the hub.
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
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);
400 JsonObject jsonObject = JsonParser.parseString(jsonResponse).getAsJsonObject();
401 JsonElement scheduledEventElement = jsonObject.get("scheduledEvent");
402 if (scheduledEventElement == null) {
403 throw new HubInvalidResponseException("Missing 'scheduledEvent' element");
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);
414 * Fetches a JSON package that describes all repeaters in the hub, and wraps it in
415 * a Repeaters class instance
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
422 public Repeaters getRepeaters()
423 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
424 String json = invoke(HttpMethod.GET, repeaters, null, null);
426 Repeaters repeaters = gson.fromJson(json, Repeaters.class);
427 if (repeaters == null) {
428 throw new HubInvalidResponseException("Missing repeaters response");
430 List<RepeaterData> repeaterData = repeaters.repeaterData;
431 if (repeaterData == null) {
432 throw new HubInvalidResponseException("Missing 'repeaters.repeaterData' element");
435 } catch (JsonParseException e) {
436 throw new HubInvalidResponseException("Error parsing repeaters response", e);
441 * Fetches a JSON package that describes a specific repeater in the hub, and wraps it
442 * in a RepeaterData class instance
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
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);
456 private RepeaterData repeaterDataFromJson(String json) throws HubInvalidResponseException {
458 Repeater repeater = gson.fromJson(json, Repeater.class);
459 if (repeater == null) {
460 throw new HubInvalidResponseException("Missing repeater response");
462 RepeaterData repeaterData = repeater.repeater;
463 if (repeaterData == null) {
464 throw new HubInvalidResponseException("Missing 'repeater.repeater' element");
467 } catch (JsonParseException e) {
468 throw new HubInvalidResponseException("Error parsing repeater response", e);
473 * Instructs the hub to identify a specific repeater by blinking
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
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);
489 * Enables or disables blinking for a repeater
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
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);
506 * Invoke a call on the hub server to retrieve information or send a command
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
516 private synchronized String invoke(HttpMethod method, String url, @Nullable Query query,
517 @Nullable String jsonCommand) throws HubMaintenanceException, HubProcessingException {
518 if (logger.isTraceEnabled()) {
520 logger.trace("API command {} {}{}", method, url, query);
522 logger.trace("API command {} {}", method, url);
524 if (jsonCommand != null) {
525 logger.trace("JSON command = {}", jsonCommand);
528 Request request = httpClient.newRequest(url).method(method).header("Connection", "close").accept("*/*");
530 request.param(query.getKey(), query.getValue());
532 if (jsonCommand != null) {
533 request.header(HttpHeader.CONTENT_TYPE, "application/json").content(new StringContentProvider(jsonCommand));
535 ContentResponse response;
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");
544 throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
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");
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));
557 String jsonResponse = response.getContentAsString();
558 if ("".equals(jsonResponse)) {
559 logger.warn("Hub returned no content");
560 throw new HubProcessingException("Missing response entity");
562 if (logger.isTraceEnabled()) {
563 logger.trace("JSON response = {}", jsonResponse);
569 * Fetches a JSON package that describes a specific shade in the hub, and wraps it
570 * in a Shade class instance
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
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);
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
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
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);
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
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
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);
619 Survey survey = gson.fromJson(jsonResponse, Survey.class);
620 if (survey == null) {
621 throw new HubInvalidResponseException("Missing survey response");
624 } catch (JsonParseException e) {
625 throw new HubInvalidResponseException("Error parsing survey response", e);
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
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
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);