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