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