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