]> git.basschouten.com Git - openhab-addons.git/blob
16efb896a6a986775ea0836c5f57146c81fa25e8
[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 HubMaintenanceException
241      * @throws HubProcessingException
242      */
243     private synchronized String invoke(HttpMethod method, String url, @Nullable Query query,
244             @Nullable String jsonCommand) throws HubMaintenanceException, HubProcessingException {
245         if (logger.isTraceEnabled()) {
246             logger.trace("API command {} {}", method, url);
247             if (jsonCommand != null) {
248                 logger.trace("JSON command = {}", jsonCommand);
249             }
250         }
251         Request request = httpClient.newRequest(url).method(method).header("Connection", "close").accept("*/*");
252         if (query != null) {
253             request.param(query.getKey(), query.getValue());
254         }
255         if (jsonCommand != null) {
256             request.header(HttpHeader.CONTENT_TYPE, "application/json").content(new StringContentProvider(jsonCommand));
257         }
258         ContentResponse response;
259         try {
260             response = request.send();
261         } catch (InterruptedException | TimeoutException | ExecutionException e) {
262             if (Instant.now().isBefore(maintenanceScheduledEnd)) {
263                 // throw "softer" exception during maintenance window
264                 logger.debug("Hub still undergoing maintenance");
265                 throw new HubMaintenanceException("Hub still undergoing maintenance");
266             }
267             throw new HubProcessingException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
268         }
269         int statusCode = response.getStatus();
270         if (statusCode == HttpStatus.LOCKED_423) {
271             // set end of maintenance window, and throw a "softer" exception
272             maintenanceScheduledEnd = Instant.now().plusSeconds(maintenancePeriod);
273             logger.debug("Hub undergoing maintenance");
274             throw new HubMaintenanceException("Hub undergoing maintenance");
275         }
276         if (statusCode != HttpStatus.OK_200) {
277             logger.warn("Hub returned HTTP {} {}", statusCode, response.getReason());
278             throw new HubProcessingException(String.format("HTTP %d error", statusCode));
279         }
280         String jsonResponse = response.getContentAsString();
281         if ("".equals(jsonResponse)) {
282             logger.warn("Hub returned no content");
283             throw new HubProcessingException("Missing response entity");
284         }
285         if (logger.isTraceEnabled()) {
286             logger.trace("JSON response = {}", jsonResponse);
287         }
288         return jsonResponse;
289     }
290
291     /**
292      * Fetches a JSON package that describes a specific shade in the hub, and wraps it
293      * in a Shade class instance
294      *
295      * @param shadeId id of the shade to be fetched
296      * @return Shade class instance
297      * @throws JsonParseException if there is a JSON parsing error
298      * @throws HubProcessingException if there is any processing error
299      * @throws HubMaintenanceException if the hub is down for maintenance
300      */
301     public @Nullable Shade getShade(int shadeId)
302             throws JsonParseException, HubProcessingException, HubMaintenanceException {
303         String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), null, null);
304         return gson.fromJson(json, Shade.class);
305     }
306
307     /**
308      * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
309      * a specific shade's position; fetches a JSON package that describes that shade,
310      * and wraps it in a Shade class instance
311      *
312      * @param shadeId id of the shade to be refreshed
313      * @return Shade class instance
314      * @throws JsonParseException if there is a JSON parsing error
315      * @throws HubProcessingException if there is any processing error
316      * @throws HubMaintenanceException if the hub is down for maintenance
317      */
318     public @Nullable Shade refreshShadePosition(int shadeId)
319             throws JsonParseException, HubProcessingException, HubMaintenanceException {
320         String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
321                 Query.of("refresh", Boolean.toString(true)), null);
322         return gson.fromJson(json, Shade.class);
323     }
324
325     /**
326      * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
327      * a specific shade's battery level; fetches a JSON package that describes that shade,
328      * and wraps it in a Shade class instance
329      *
330      * @param shadeId id of the shade to be refreshed
331      * @return Shade class instance
332      * @throws JsonParseException if there is a JSON parsing error
333      * @throws HubProcessingException if there is any processing error
334      * @throws HubMaintenanceException if the hub is down for maintenance
335      */
336     public @Nullable Shade refreshShadeBatteryLevel(int shadeId)
337             throws JsonParseException, HubProcessingException, HubMaintenanceException {
338         String json = invoke(HttpMethod.GET, shades + Integer.toString(shadeId),
339                 Query.of("updateBatteryLevel", Boolean.toString(true)), null);
340         return gson.fromJson(json, Shade.class);
341     }
342
343     /**
344      * Tells the hub to stop movement of a specific shade
345      *
346      * @param shadeId id of the shade to be stopped
347      * @throws HubProcessingException if there is any processing error
348      * @throws HubMaintenanceException if the hub is down for maintenance
349      */
350     public void stopShade(int shadeId) throws HubProcessingException, HubMaintenanceException {
351         String json = gson.toJson(new ShadeStop(shadeId));
352         invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json);
353     }
354 }