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