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