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