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