]> git.basschouten.com Git - openhab-addons.git/blob
8fee72166c96aaf6ae50d633f404cb846a1ea22e
[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         sceneCollectionActivate = base + "sceneCollections";
113         sceneCollections = base + "sceneCollections/";
114         scheduledEvents = base + "scheduledevents";
115         this.httpClient = httpClient;
116     }
117
118     /**
119      * Fetches a JSON package that describes all shades in the hub, and wraps it in
120      * a Shades class instance
121      *
122      * @return Shades class instance
123      * @throws JsonParseException if there is a JSON parsing error
124      * @throws HubProcessingException if there is any processing error
125      * @throws HubMaintenanceException if the hub is down for maintenance
126      */
127     public @Nullable Shades getShades() throws JsonParseException, HubProcessingException, HubMaintenanceException {
128         String json = invoke(HttpMethod.GET, shades, null, null);
129         return gson.fromJson(json, Shades.class);
130     }
131
132     /**
133      * Instructs the hub to move a specific shade
134      *
135      * @param shadeId id of the shade to be moved
136      * @param position instance of ShadePosition containing the new position
137      * @throws HubProcessingException if there is any processing error
138      * @throws HubMaintenanceException if the hub is down for maintenance
139      */
140     public void moveShade(int shadeId, ShadePosition position) throws HubProcessingException, HubMaintenanceException {
141         String json = gson.toJson(new ShadeMove(shadeId, position));
142         invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json);
143     }
144
145     /**
146      * Fetches a JSON package that describes all scenes in the hub, and wraps it in
147      * a Scenes class instance
148      *
149      * @return Scenes class instance
150      * @throws JsonParseException if there is a JSON parsing error
151      * @throws HubProcessingException if there is any processing error
152      * @throws HubMaintenanceException if the hub is down for maintenance
153      */
154     public @Nullable Scenes getScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
155         String json = invoke(HttpMethod.GET, scenes, null, null);
156         return gson.fromJson(json, Scenes.class);
157     }
158
159     /**
160      * Instructs the hub to execute a specific scene
161      *
162      * @param sceneId id of the scene to be executed
163      * @throws HubProcessingException if there is any processing error
164      * @throws HubMaintenanceException if the hub is down for maintenance
165      */
166     public void activateScene(int sceneId) throws HubProcessingException, HubMaintenanceException {
167         invoke(HttpMethod.GET, sceneActivate, Query.of("sceneId", Integer.toString(sceneId)), null);
168     }
169
170     /**
171      * Fetches a JSON package that describes all scene collections in the hub, and wraps it in
172      * a SceneCollections class instance
173      *
174      * @return SceneCollections class instance
175      * @throws JsonParseException if there is a JSON parsing error
176      * @throws HubProcessingException if there is any processing error
177      * @throws HubMaintenanceException if the hub is down for maintenance
178      */
179     public @Nullable SceneCollections getSceneCollections()
180             throws JsonParseException, HubProcessingException, HubMaintenanceException {
181         String json = invoke(HttpMethod.GET, sceneCollections, null, null);
182         return gson.fromJson(json, SceneCollections.class);
183     }
184
185     /**
186      * Instructs the hub to execute a specific scene collection
187      *
188      * @param sceneCollectionId id of the scene collection 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 activateSceneCollection(int sceneCollectionId) throws HubProcessingException, HubMaintenanceException {
193         invoke(HttpMethod.GET, sceneCollectionActivate,
194                 Query.of("sceneCollectionId", Integer.toString(sceneCollectionId)), null);
195     }
196
197     /**
198      * Fetches a JSON package that describes all scheduled events in the hub, and wraps it in
199      * a ScheduledEvents class instance
200      *
201      * @return ScheduledEvents class instance
202      * @throws JsonParseException if there is a JSON parsing error
203      * @throws HubProcessingException if there is any processing error
204      * @throws HubMaintenanceException if the hub is down for maintenance
205      */
206     public @Nullable ScheduledEvents getScheduledEvents()
207             throws JsonParseException, HubProcessingException, HubMaintenanceException {
208         String json = invoke(HttpMethod.GET, scheduledEvents, null, null);
209         return gson.fromJson(json, ScheduledEvents.class);
210     }
211
212     /**
213      * Enables or disables a scheduled event in the hub.
214      * 
215      * @param scheduledEventId id of the scheduled event to be enabled or disabled
216      * @param enable true to enable scheduled event, false to disable
217      * @throws JsonParseException if there is a JSON parsing error
218      * @throws JsonSyntaxException if there is a JSON syntax error
219      * @throws HubProcessingException if there is any processing error
220      * @throws HubMaintenanceException if the hub is down for maintenance
221      */
222     public void enableScheduledEvent(int scheduledEventId, boolean enable)
223             throws JsonParseException, HubProcessingException, HubMaintenanceException {
224         String uri = scheduledEvents + "/" + scheduledEventId;
225         String json = invoke(HttpMethod.GET, uri, null, null);
226         JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
227         JsonObject scheduledEventObject = jsonObject.get("scheduledEvent").getAsJsonObject();
228         scheduledEventObject.addProperty("enabled", enable);
229         invoke(HttpMethod.PUT, uri, null, jsonObject.toString());
230     }
231
232     /**
233      * Invoke a call on the hub server to retrieve information or send a command
234      *
235      * @param method GET or PUT
236      * @param url the host url to be called
237      * @param query the http query parameter
238      * @param jsonCommand the request command content (as a json string)
239      * @return the response content (as a json string)
240      * @throws HubProcessingException
241      * @throws HubMaintenanceException
242      * @throws HubProcessingException
243      */
244     private synchronized String invoke(HttpMethod method, String url, @Nullable Query query,
245             @Nullable String jsonCommand) throws HubMaintenanceException, HubProcessingException {
246         if (logger.isTraceEnabled()) {
247             logger.trace("API command {} {}", method, url);
248             if (jsonCommand != null) {
249                 logger.trace("JSON command = {}", jsonCommand);
250             }
251         }
252         Request request = httpClient.newRequest(url).method(method).header("Connection", "close").accept("*/*");
253         if (query != null) {
254             request.param(query.getKey(), query.getValue());
255         }
256         if (jsonCommand != null) {
257             request.header(HttpHeader.CONTENT_TYPE, "application/json").content(new StringContentProvider(jsonCommand));
258         }
259         ContentResponse response;
260         try {
261             response = request.send();
262         } catch (InterruptedException | TimeoutException | ExecutionException e) {
263             if (Instant.now().isBefore(maintenanceScheduledEnd)) {
264                 // throw "softer" exception during maintenance window
265                 logger.debug("Hub still undergoing maintenance");
266                 throw new HubMaintenanceException("Hub still undergoing maintenance");
267             }
268             throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
269         }
270         int statusCode = response.getStatus();
271         if (statusCode == HttpStatus.LOCKED_423) {
272             // set end of maintenance window, and throw a "softer" exception
273             maintenanceScheduledEnd = Instant.now().plusSeconds(maintenancePeriod);
274             logger.debug("Hub undergoing maintenance");
275             throw new HubMaintenanceException("Hub undergoing maintenance");
276         }
277         if (statusCode != HttpStatus.OK_200) {
278             logger.warn("Hub returned HTTP {} {}", statusCode, response.getReason());
279             throw new HubProcessingException(String.format("HTTP %d error", statusCode));
280         }
281         String jsonResponse = response.getContentAsString();
282         if ("".equals(jsonResponse)) {
283             logger.warn("Hub returned no content");
284             throw new HubProcessingException("Missing response entity");
285         }
286         if (logger.isTraceEnabled()) {
287             logger.trace("JSON response = {}", jsonResponse);
288         }
289         return jsonResponse;
290     }
291
292     /**
293      * Fetches a JSON package that describes a specific shade in the hub, and wraps it
294      * in a Shade class instance
295      *
296      * @param shadeId id of the shade to be fetched
297      * @return Shade class instance
298      * @throws JsonParseException if there is a JSON parsing error
299      * @throws HubProcessingException if there is any processing error
300      * @throws HubMaintenanceException if the hub is down for maintenance
301      */
302     public @Nullable Shade getShade(int shadeId)
303             throws JsonParseException, HubProcessingException, HubMaintenanceException {
304         String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), null, null);
305         return gson.fromJson(json, Shade.class);
306     }
307
308     /**
309      * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
310      * a specific shade's position; fetches a JSON package that describes that shade,
311      * and wraps it in a Shade class instance
312      *
313      * @param shadeId id of the shade to be refreshed
314      * @return Shade class instance
315      * @throws JsonParseException if there is a JSON parsing error
316      * @throws HubProcessingException if there is any processing error
317      * @throws HubMaintenanceException if the hub is down for maintenance
318      */
319     public @Nullable Shade refreshShadePosition(int shadeId)
320             throws JsonParseException, HubProcessingException, HubMaintenanceException {
321         String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
322                 Query.of("refresh", Boolean.toString(true)), null);
323         return gson.fromJson(json, Shade.class);
324     }
325
326     /**
327      * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
328      * a specific shade's battery level; fetches a JSON package that describes that shade,
329      * and wraps it in a Shade class instance
330      *
331      * @param shadeId id of the shade to be refreshed
332      * @return Shade class instance
333      * @throws JsonParseException if there is a JSON parsing error
334      * @throws HubProcessingException if there is any processing error
335      * @throws HubMaintenanceException if the hub is down for maintenance
336      */
337     public @Nullable Shade refreshShadeBatteryLevel(int shadeId)
338             throws JsonParseException, HubProcessingException, HubMaintenanceException {
339         String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
340                 Query.of("updateBatteryLevel", Boolean.toString(true)), null);
341         return gson.fromJson(json, Shade.class);
342     }
343
344     /**
345      * Tells the hub to stop movement of a specific shade
346      *
347      * @param shadeId id of the shade to be stopped
348      * @throws HubProcessingException if there is any processing error
349      * @throws HubMaintenanceException if the hub is down for maintenance
350      */
351     public void stopShade(int shadeId) throws HubProcessingException, HubMaintenanceException {
352         String json = gson.toJson(new ShadeStop(shadeId));
353         invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json);
354     }
355 }