2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.opensprinkler.internal.api;
15 import static org.openhab.binding.opensprinkler.internal.api.OpenSprinklerApiConstants.*;
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;
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;
38 import com.google.gson.Gson;
39 import com.google.gson.annotations.SerializedName;
42 * The {@link OpenSprinklerHttpApiV100} class is used for communicating with the
43 * OpenSprinkler API for firmware versions less than 2.1.0
45 * @author Chris Graham - Initial contribution
46 * @author Florian Schmidt - Allow https URLs and basic auth
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;
55 protected int firmwareVersion = -1;
56 protected int numberOfStations = DEFAULT_STATION_COUNT;
58 protected boolean isInManualMode = false;
60 private final Gson gson = new Gson();
61 protected HttpRequestSender http;
64 * Constructor for the OpenSprinkler API class to create a connection to the
65 * OpenSprinkler device for control and obtaining status info.
67 * @param hostname Hostname or IP address as a String of the OpenSprinkler
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
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.");
80 if (config.port < 1 || config.port > 65535) {
81 throw new GeneralApiException("The given port is invalid.");
83 if (config.password == null) {
84 throw new GeneralApiException("The given password is null.");
87 if (config.hostname.startsWith(HTTP_REQUEST_URL_PREFIX)
88 || config.hostname.startsWith(HTTPS_REQUEST_URL_PREFIX)) {
89 this.hostname = config.hostname;
91 this.hostname = HTTP_REQUEST_URL_PREFIX + config.hostname;
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);
101 public boolean isManualModeEnabled() {
102 return isInManualMode;
106 public void enterManualMode() throws CommunicationApiException {
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());
114 this.firmwareVersion = getFirmwareVersion();
115 this.numberOfStations = getNumberOfStations();
117 isInManualMode = true;
121 public void leaveManualMode() throws CommunicationApiException {
122 isInManualMode = false;
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());
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.");
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());
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.");
154 http.sendHttpGet(getBaseUrl() + "sn" + station + "=0", null);
158 public boolean isStationOpen(int station) throws GeneralApiException, CommunicationApiException {
159 String returnContent;
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.");
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());
173 return returnContent != null && returnContent.equals("1");
177 public boolean isRainDetected() throws CommunicationApiException {
178 if (statusInfo().rs == 1) {
186 public int currentDraw() throws CommunicationApiException, NoCurrentDrawSensorException {
187 JcResponse info = statusInfo();
188 if (info.curr == null) {
189 throw new NoCurrentDrawSensorException();
195 public int waterLevel() throws CommunicationApiException {
196 JoResponse info = getOptions();
201 public int getNumberOfStations() throws CommunicationApiException {
202 String returnContent;
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());
211 this.numberOfStations = Parse.jsonInt(returnContent, JSON_OPTION_STATION_COUNT);
213 return this.numberOfStations;
217 public int getFirmwareVersion() throws CommunicationApiException {
220 JoResponse info = getOptions();
221 this.firmwareVersion = info.fwv;
222 } catch (Exception exp) {
223 this.firmwareVersion = -1;
226 return this.firmwareVersion;
230 * Returns the hostname and port formatted URL as a String.
232 * @return String representation of the OpenSprinkler API URL.
234 protected String getBaseUrl() {
235 return hostname + ":" + port + "/";
239 * Returns the required URL parameters required for every API call.
241 * @return String representation of the parameters needed during an API call.
243 protected String getRequestRequiredOptions() {
244 return CMD_PASSWORD + this.password;
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())
254 private JcResponse statusInfo() throws CommunicationApiException {
255 String returnContent;
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());
264 JcResponse resp = gson.fromJson(returnContent, JcResponse.class);
268 private static class JcResponse {
269 public List<List<Integer>> ps;
270 @SerializedName(value = "sn1", alternate = "rs")
275 private JoResponse getOptions() throws CommunicationApiException {
276 String returnContent;
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());
285 JoResponse resp = gson.fromJson(returnContent, JoResponse.class);
289 private static class JoResponse {
295 * This class contains helper methods for communicating HTTP GET and HTTP POST
298 * @author Chris Graham - Initial contribution
299 * @author Florian Schmidt - Reduce visibility of Http communication to Api
301 protected class HttpRequestSender {
302 private static final int HTTP_OK_CODE = 200;
303 private static final String USER_AGENT = "Mozilla/5.0";
305 private final HttpClient httpClient;
307 public HttpRequestSender(HttpClient httpClient) {
308 this.httpClient = httpClient;
312 * Given a URL and a set parameters, send a HTTP GET request to the URL location
313 * created by the URL and parameters.
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.
321 public String sendHttpGet(String url, String urlParameters) throws CommunicationApiException {
322 String location = null;
324 if (urlParameters != null) {
325 location = url + "?" + urlParameters;
330 ContentResponse response;
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());
337 if (response.getStatus() != HTTP_OK_CODE) {
338 throw new CommunicationApiException(
339 "Error sending HTTP GET request to " + url + ". Got response code: " + response.getStatus());
342 return response.getContentAsString();
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);
356 * Given a URL and a set parameters, send a HTTP POST request to the URL
357 * location created by the URL and parameters.
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.
365 public String sendHttpPost(String url, String urlParameters) throws CommunicationApiException {
366 ContentResponse response;
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());
374 if (response.getStatus() != HTTP_OK_CODE) {
375 throw new CommunicationApiException(
376 "Error sending HTTP POST request to " + url + ". Got response code: " + response.getStatus());
379 return response.getContentAsString();