]> git.basschouten.com Git - openhab-addons.git/blob
0e4a1dc1518ce546e3aa954f04a1297c96b1b522
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.opensprinkler.internal.api;
14
15 import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiConstants.*;
16
17 import java.math.BigDecimal;
18 import java.nio.charset.StandardCharsets;
19 import java.util.Base64;
20 import java.util.List;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeoutException;
23 import java.util.stream.Collectors;
24
25 import org.eclipse.jetty.client.HttpClient;
26 import org.eclipse.jetty.client.api.ContentResponse;
27 import org.eclipse.jetty.client.api.Request;
28 import org.eclipse.jetty.client.util.StringContentProvider;
29 import org.eclipse.jetty.http.HttpHeader;
30 import org.eclipse.jetty.http.HttpMethod;
31 import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
32 import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
33 import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
34 import org.openhab.binding.opensprinkler.internal.model.NoCurrentDrawSensorException;
35 import org.openhab.binding.opensprinkler.internal.model.StationProgram;
36 import org.openhab.binding.opensprinkler.internal.util.Parse;
37
38 import com.google.gson.Gson;
39 import com.google.gson.annotations.SerializedName;
40
41 /**
42  * The {@link OpenSprinklerHttpApiV100} class is used for communicating with the
43  * OpenSprinkler API for firmware versions less than 2.1.0
44  *
45  * @author Chris Graham - Initial contribution
46  * @author Florian Schmidt - Allow https URLs and basic auth
47  */
48 class OpenSprinklerHttpApiV100 implements OpenSprinklerApi {
49     protected final String hostname;
50     protected final int port;
51     protected final String password;
52     protected final String basicUsername;
53     protected final String basicPassword;
54
55     protected int firmwareVersion = -1;
56     protected int numberOfStations = DEFAULT_STATION_COUNT;
57
58     protected boolean isInManualMode = false;
59
60     private final Gson gson = new Gson();
61     protected HttpRequestSender http;
62
63     /**
64      * Constructor for the OpenSprinkler API class to create a connection to the
65      * OpenSprinkler device for control and obtaining status info.
66      *
67      * @param hostname Hostname or IP address as a String of the OpenSprinkler
68      *            device.
69      * @param port The port number the OpenSprinkler API is listening on.
70      * @param password Admin password for the OpenSprinkler device.
71      * @param basicUsername only needed if basic auth is required
72      * @param basicPassword only needed if basic auth is required
73      * @throws Exception
74      */
75     OpenSprinklerHttpApiV100(final HttpClient httpClient, final OpenSprinklerHttpInterfaceConfig config)
76             throws GeneralApiException {
77         if (config.hostname == null) {
78             throw new GeneralApiException("The given url is null.");
79         }
80         if (config.port < 1 || config.port > 65535) {
81             throw new GeneralApiException("The given port is invalid.");
82         }
83         if (config.password == null) {
84             throw new GeneralApiException("The given password is null.");
85         }
86
87         if (config.hostname.startsWith(HTTP_REQUEST_URL_PREFIX)
88                 || config.hostname.startsWith(HTTPS_REQUEST_URL_PREFIX)) {
89             this.hostname = config.hostname;
90         } else {
91             this.hostname = HTTP_REQUEST_URL_PREFIX + config.hostname;
92         }
93         this.port = config.port;
94         this.password = config.password;
95         this.basicUsername = config.basicUsername;
96         this.basicPassword = config.basicPassword;
97         this.http = new HttpRequestSender(httpClient);
98     }
99
100     @Override
101     public boolean isManualModeEnabled() {
102         return isInManualMode;
103     }
104
105     @Override
106     public void enterManualMode() throws CommunicationApiException {
107         try {
108             http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_ENABLE_MANUAL_MODE);
109         } catch (Exception exp) {
110             throw new CommunicationApiException(
111                     "There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
112         }
113
114         this.firmwareVersion = getFirmwareVersion();
115         this.numberOfStations = getNumberOfStations();
116
117         isInManualMode = true;
118     }
119
120     @Override
121     public void leaveManualMode() throws CommunicationApiException {
122         isInManualMode = false;
123
124         try {
125             http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_DISABLE_MANUAL_MODE);
126         } catch (Exception exp) {
127             throw new CommunicationApiException(
128                     "There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
129         }
130     }
131
132     @Override
133     public void openStation(int station, BigDecimal duration) throws CommunicationApiException, GeneralApiException {
134         if (station < 0 || station >= numberOfStations) {
135             throw new GeneralApiException("This OpenSprinkler device only has " + this.numberOfStations
136                     + " but station " + station + " was requested to be opened.");
137         }
138
139         try {
140             http.sendHttpGet(getBaseUrl() + "sn" + station + "=1&t=" + duration, null);
141         } catch (Exception exp) {
142             throw new CommunicationApiException(
143                     "There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
144         }
145     }
146
147     @Override
148     public void closeStation(int station) throws CommunicationApiException, GeneralApiException {
149         if (station < 0 || station >= numberOfStations) {
150             throw new GeneralApiException("This OpenSprinkler device only has " + this.numberOfStations
151                     + " but station " + station + " was requested to be closed.");
152         }
153
154         http.sendHttpGet(getBaseUrl() + "sn" + station + "=0", null);
155     }
156
157     @Override
158     public boolean isStationOpen(int station) throws GeneralApiException, CommunicationApiException {
159         String returnContent;
160
161         if (station < 0 || station >= numberOfStations) {
162             throw new GeneralApiException("This OpenSprinkler device only has " + this.numberOfStations
163                     + " but station " + station + " was requested for a status update.");
164         }
165
166         try {
167             returnContent = http.sendHttpGet(getBaseUrl() + "sn" + station, null);
168         } catch (Exception exp) {
169             throw new CommunicationApiException(
170                     "There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
171         }
172
173         return returnContent != null && returnContent.equals("1");
174     }
175
176     @Override
177     public boolean isRainDetected() throws CommunicationApiException {
178         if (statusInfo().rs == 1) {
179             return true;
180         } else {
181             return false;
182         }
183     }
184
185     @Override
186     public int currentDraw() throws CommunicationApiException, NoCurrentDrawSensorException {
187         JcResponse info = statusInfo();
188         if (info.curr == null) {
189             throw new NoCurrentDrawSensorException();
190         }
191         return info.curr;
192     }
193
194     @Override
195     public int waterLevel() throws CommunicationApiException {
196         JoResponse info = getOptions();
197         return info.wl;
198     }
199
200     @Override
201     public int getNumberOfStations() throws CommunicationApiException {
202         String returnContent;
203
204         try {
205             returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATION_INFO, getRequestRequiredOptions());
206         } catch (Exception exp) {
207             throw new CommunicationApiException(
208                     "There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
209         }
210
211         this.numberOfStations = Parse.jsonInt(returnContent, JSON_OPTION_STATION_COUNT);
212
213         return this.numberOfStations;
214     }
215
216     @Override
217     public int getFirmwareVersion() throws CommunicationApiException {
218
219         try {
220             JoResponse info = getOptions();
221             this.firmwareVersion = info.fwv;
222         } catch (Exception exp) {
223             this.firmwareVersion = -1;
224         }
225
226         return this.firmwareVersion;
227     }
228
229     /**
230      * Returns the hostname and port formatted URL as a String.
231      *
232      * @return String representation of the OpenSprinkler API URL.
233      */
234     protected String getBaseUrl() {
235         return hostname + ":" + port + "/";
236     }
237
238     /**
239      * Returns the required URL parameters required for every API call.
240      *
241      * @return String representation of the parameters needed during an API call.
242      */
243     protected String getRequestRequiredOptions() {
244         return CMD_PASSWORD + this.password;
245     }
246
247     @Override
248     public StationProgram retrieveProgram(int station) throws CommunicationApiException {
249         JcResponse resp = statusInfo();
250         return resp.ps.stream().map(values -> new StationProgram(values.get(1))).collect(Collectors.toList())
251                 .get(station);
252     }
253
254     private JcResponse statusInfo() throws CommunicationApiException {
255         String returnContent;
256
257         try {
258             returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATUS_INFO, getRequestRequiredOptions());
259         } catch (CommunicationApiException exp) {
260             throw new CommunicationApiException(
261                     "There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
262         }
263
264         JcResponse resp = gson.fromJson(returnContent, JcResponse.class);
265         return resp;
266     }
267
268     private static class JcResponse {
269         public List<List<Integer>> ps;
270         @SerializedName(value = "sn1", alternate = "rs")
271         public int rs;
272         public Integer curr;
273     }
274
275     private JoResponse getOptions() throws CommunicationApiException {
276         String returnContent;
277
278         try {
279             returnContent = http.sendHttpGet(getBaseUrl() + CMD_OPTIONS_INFO, getRequestRequiredOptions());
280         } catch (CommunicationApiException exp) {
281             throw new CommunicationApiException(
282                     "There was a problem in the HTTP communication with the OpenSprinkler API: " + exp.getMessage());
283         }
284
285         JoResponse resp = gson.fromJson(returnContent, JoResponse.class);
286         return resp;
287     }
288
289     private static class JoResponse {
290         public int wl;
291         public int fwv;
292     }
293
294     /**
295      * This class contains helper methods for communicating HTTP GET and HTTP POST
296      * requests.
297      *
298      * @author Chris Graham - Initial contribution
299      * @author Florian Schmidt - Reduce visibility of Http communication to Api
300      */
301     protected class HttpRequestSender {
302         private static final int HTTP_OK_CODE = 200;
303         private static final String USER_AGENT = "Mozilla/5.0";
304
305         private final HttpClient httpClient;
306
307         public HttpRequestSender(HttpClient httpClient) {
308             this.httpClient = httpClient;
309         }
310
311         /**
312          * Given a URL and a set parameters, send a HTTP GET request to the URL location
313          * created by the URL and parameters.
314          *
315          * @param url The URL to send a GET request to.
316          * @param urlParameters List of parameters to use in the URL for the GET
317          *            request. Null if no parameters.
318          * @return String contents of the response for the GET request.
319          * @throws Exception
320          */
321         public String sendHttpGet(String url, String urlParameters) throws CommunicationApiException {
322             String location = null;
323
324             if (urlParameters != null) {
325                 location = url + "?" + urlParameters;
326             } else {
327                 location = url;
328             }
329
330             ContentResponse response;
331             try {
332                 response = withGeneralProperties(httpClient.newRequest(location)).method(HttpMethod.GET).send();
333             } catch (InterruptedException | TimeoutException | ExecutionException e) {
334                 throw new CommunicationApiException("Request to OpenSprinkler device failed: " + e.getMessage());
335             }
336
337             if (response.getStatus() != HTTP_OK_CODE) {
338                 throw new CommunicationApiException(
339                         "Error sending HTTP GET request to " + url + ". Got response code: " + response.getStatus());
340             }
341
342             return response.getContentAsString();
343         }
344
345         private Request withGeneralProperties(Request request) {
346             request.header(HttpHeader.USER_AGENT, USER_AGENT);
347             if (basicUsername != null && basicPassword != null) {
348                 String encoded = Base64.getEncoder()
349                         .encodeToString((basicUsername + ":" + basicPassword).getBytes(StandardCharsets.UTF_8));
350                 request.header(HttpHeader.AUTHORIZATION, "Basic " + encoded);
351             }
352             return request;
353         }
354
355         /**
356          * Given a URL and a set parameters, send a HTTP POST request to the URL
357          * location created by the URL and parameters.
358          *
359          * @param url The URL to send a POST request to.
360          * @param urlParameters List of parameters to use in the URL for the POST
361          *            request. Null if no parameters.
362          * @return String contents of the response for the POST request.
363          * @throws Exception
364          */
365         public String sendHttpPost(String url, String urlParameters) throws CommunicationApiException {
366             ContentResponse response;
367             try {
368                 response = withGeneralProperties(httpClient.newRequest(url)).method(HttpMethod.POST)
369                         .content(new StringContentProvider(urlParameters)).send();
370             } catch (InterruptedException | TimeoutException | ExecutionException e) {
371                 throw new CommunicationApiException("Request to OpenSprinkler device failed: " + e.getMessage());
372             }
373
374             if (response.getStatus() != HTTP_OK_CODE) {
375                 throw new CommunicationApiException(
376                         "Error sending HTTP POST request to " + url + ". Got response code: " + response.getStatus());
377             }
378
379             return response.getContentAsString();
380         }
381     }
382 }