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