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