2 * Copyright (c) 2010-2023 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.OpenSprinklerBindingConstants.CMD_DISABLE_MANUAL_MODE;
16 import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.CMD_ENABLE_MANUAL_MODE;
17 import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.CMD_OPTIONS_INFO;
18 import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.CMD_PASSWORD;
19 import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.CMD_STATION_INFO;
20 import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.CMD_STATUS_INFO;
21 import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.DEFAULT_STATION_COUNT;
22 import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.HTTPS_REQUEST_URL_PREFIX;
23 import static org.openhab.binding.opensprinkler.internal.OpenSprinklerBindingConstants.HTTP_REQUEST_URL_PREFIX;
25 import java.math.BigDecimal;
26 import java.nio.charset.StandardCharsets;
27 import java.util.Base64;
28 import java.util.List;
29 import java.util.concurrent.ExecutionException;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.TimeoutException;
32 import java.util.stream.Collectors;
34 import javax.measure.quantity.Time;
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.eclipse.jetty.client.HttpClient;
39 import org.eclipse.jetty.client.api.ContentResponse;
40 import org.eclipse.jetty.client.api.Request;
41 import org.eclipse.jetty.client.util.StringContentProvider;
42 import org.eclipse.jetty.http.HttpHeader;
43 import org.eclipse.jetty.http.HttpMethod;
44 import org.openhab.binding.opensprinkler.internal.OpenSprinklerState;
45 import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JcResponse;
46 import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JnResponse;
47 import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JoResponse;
48 import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JsResponse;
49 import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
50 import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
51 import org.openhab.binding.opensprinkler.internal.api.exception.UnauthorizedApiException;
52 import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
53 import org.openhab.binding.opensprinkler.internal.model.StationProgram;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.library.types.QuantityType;
56 import org.openhab.core.library.unit.Units;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.StateOption;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
62 import com.google.gson.Gson;
63 import com.google.gson.JsonSyntaxException;
66 * The {@link OpenSprinklerHttpApiV100} class is used for communicating with the
67 * OpenSprinkler API for firmware versions less than 2.1.0
69 * @author Chris Graham - Initial contribution
70 * @author Florian Schmidt - Allow https URLs and basic auth
73 class OpenSprinklerHttpApiV100 implements OpenSprinklerApi {
74 protected final Logger logger = LoggerFactory.getLogger(this.getClass());
75 protected final String hostname;
76 protected final OpenSprinklerHttpInterfaceConfig config;
77 protected String password;
78 protected OpenSprinklerState state = new OpenSprinklerState();
79 protected int numberOfStations = DEFAULT_STATION_COUNT;
80 protected boolean isInManualMode = false;
81 protected final Gson gson = new Gson();
82 protected HttpRequestSender http;
85 * Constructor for the OpenSprinkler API class to create a connection to the
86 * OpenSprinkler device for control and obtaining status info.
88 * @param hostname Hostname or IP address as a String of the OpenSprinkler
90 * @param port The port number the OpenSprinkler API is listening on.
91 * @param password Admin password for the OpenSprinkler device.
92 * @param basicUsername only needed if basic auth is required
93 * @param basicPassword only needed if basic auth is required
96 OpenSprinklerHttpApiV100(final HttpClient httpClient, final OpenSprinklerHttpInterfaceConfig config)
97 throws CommunicationApiException, UnauthorizedApiException {
98 if (config.hostname.startsWith(HTTP_REQUEST_URL_PREFIX)
99 || config.hostname.startsWith(HTTPS_REQUEST_URL_PREFIX)) {
100 this.hostname = config.hostname;
102 this.hostname = HTTP_REQUEST_URL_PREFIX + config.hostname;
104 this.config = config;
105 this.password = config.password;
106 this.http = new HttpRequestSender(httpClient);
110 public boolean isManualModeEnabled() {
111 return isInManualMode;
115 public List<StateOption> getPrograms() {
116 return state.programs;
120 public List<StateOption> getStations() {
121 return state.stations;
125 public void refresh() throws CommunicationApiException, UnauthorizedApiException {
126 state.joReply = getOptions();
127 state.jsReply = getStationStatus();
128 state.jcReply = statusInfo();
129 state.jnReply = getStationNames();
133 public void enterManualMode() throws CommunicationApiException, UnauthorizedApiException {
134 http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_ENABLE_MANUAL_MODE);
135 numberOfStations = getNumberOfStations();
136 isInManualMode = true;
140 public void leaveManualMode() throws CommunicationApiException, UnauthorizedApiException {
141 http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_DISABLE_MANUAL_MODE);
142 isInManualMode = false;
146 public void openStation(int station, BigDecimal duration) throws CommunicationApiException, GeneralApiException {
147 http.sendHttpGet(getBaseUrl() + "sn" + station + "=1&t=" + duration, null);
151 public void closeStation(int station) throws CommunicationApiException, GeneralApiException {
152 http.sendHttpGet(getBaseUrl() + "sn" + station + "=0", null);
156 public boolean isStationOpen(int station) throws CommunicationApiException, GeneralApiException {
157 String returnContent = http.sendHttpGet(getBaseUrl() + "sn" + station, null);
158 return "1".equals(returnContent);
162 public void ignoreRain(int station, boolean command) throws CommunicationApiException, UnauthorizedApiException {
166 public boolean isIgnoringRain(int station) {
171 public boolean isRainDetected() {
172 return state.jcReply.rs == 1;
176 public int getSensor2State() {
177 return state.jcReply.sn2;
181 public int currentDraw() {
182 return state.jcReply.curr;
186 public int flowSensorCount() {
187 return state.jcReply.flcrt;
191 public int signalStrength() {
192 return state.jcReply.rssi;
196 public boolean getIsEnabled() {
197 return state.jcReply.en == 1;
201 public int waterLevel() {
202 return state.joReply.wl;
206 public int getNumberOfStations() {
207 numberOfStations = state.jsReply.nstations;
208 return numberOfStations;
212 public int getFirmwareVersion() throws CommunicationApiException, UnauthorizedApiException {
213 state.joReply = getOptions();
214 return state.joReply.fwv;
218 public void runProgram(Command command) throws CommunicationApiException, UnauthorizedApiException {
219 logger.warn("OpenSprinkler requires at least firmware 217 for the runProgram feature to work");
223 public void enablePrograms(Command command) throws UnauthorizedApiException, CommunicationApiException {
224 if (command == OnOffType.ON) {
225 http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&en=1");
227 http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&en=0");
232 public void resetStations() throws UnauthorizedApiException, CommunicationApiException {
233 http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&rsn=1");
237 public void setRainDelay(int hours) throws UnauthorizedApiException, CommunicationApiException {
238 http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&rd=" + hours);
242 public QuantityType<Time> getRainDelay() {
243 if (state.jcReply.rdst == 0) {
244 return new QuantityType<Time>(0, Units.SECOND);
246 long remainingTime = state.jcReply.rdst - state.jcReply.devt;
247 return new QuantityType<Time>(remainingTime, Units.SECOND);
251 * Returns the hostname and port formatted URL as a String.
253 * @return String representation of the OpenSprinkler API URL.
255 protected String getBaseUrl() {
256 return hostname + ":" + config.port + "/";
260 * Returns the required URL parameters required for every API call.
262 * @return String representation of the parameters needed during an API call.
264 protected String getRequestRequiredOptions() {
265 return CMD_PASSWORD + this.password;
269 public StationProgram retrieveProgram(int station) throws CommunicationApiException {
270 if (state.jcReply.ps != null) {
271 return state.jcReply.ps.stream().map(values -> new StationProgram(values.get(1)))
272 .collect(Collectors.toList()).get(station);
274 return new StationProgram(0);
277 private JcResponse statusInfo() throws CommunicationApiException, UnauthorizedApiException {
278 String returnContent;
281 returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATUS_INFO, getRequestRequiredOptions());
282 resp = gson.fromJson(returnContent, JcResponse.class);
284 throw new CommunicationApiException(
285 "There was a problem in the HTTP communication: jcReply was empty.");
287 } catch (JsonSyntaxException exp) {
288 throw new CommunicationApiException(
289 "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
295 private JoResponse getOptions() throws CommunicationApiException, UnauthorizedApiException {
296 String returnContent;
299 returnContent = http.sendHttpGet(getBaseUrl() + CMD_OPTIONS_INFO, getRequestRequiredOptions());
300 resp = gson.fromJson(returnContent, JoResponse.class);
302 throw new CommunicationApiException(
303 "There was a problem in the HTTP communication: joReply was empty.");
305 } catch (JsonSyntaxException exp) {
306 throw new CommunicationApiException(
307 "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
313 protected JsResponse getStationStatus() throws CommunicationApiException, UnauthorizedApiException {
314 String returnContent;
317 returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATION_INFO, getRequestRequiredOptions());
318 resp = gson.fromJson(returnContent, JsResponse.class);
320 throw new CommunicationApiException(
321 "There was a problem in the HTTP communication: jsReply was empty.");
323 } catch (JsonSyntaxException exp) {
324 throw new CommunicationApiException(
325 "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
332 public void getProgramData() throws CommunicationApiException, UnauthorizedApiException {
336 public JnResponse getStationNames() throws CommunicationApiException, UnauthorizedApiException {
337 String returnContent;
340 returnContent = http.sendHttpGet(getBaseUrl() + "jn", getRequestRequiredOptions());
341 resp = gson.fromJson(returnContent, JnResponse.class);
343 throw new CommunicationApiException(
344 "There was a problem in the HTTP communication: jnReply was empty.");
346 } catch (JsonSyntaxException exp) {
347 throw new CommunicationApiException(
348 "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
351 state.jnReply = resp;
356 * This class contains helper methods for communicating HTTP GET and HTTP POST
359 * @author Chris Graham - Initial contribution
360 * @author Florian Schmidt - Reduce visibility of Http communication to Api
362 protected class HttpRequestSender {
363 private static final int HTTP_OK_CODE = 200;
364 private static final String USER_AGENT = "Mozilla/5.0";
366 private final HttpClient httpClient;
368 public HttpRequestSender(HttpClient httpClient) {
369 this.httpClient = httpClient;
373 * Given a URL and a set parameters, send a HTTP GET request to the URL location
374 * created by the URL and parameters.
376 * @param url The URL to send a GET request to.
377 * @param urlParameters List of parameters to use in the URL for the GET
378 * request. Null if no parameters.
379 * @return String contents of the response for the GET request.
382 public String sendHttpGet(String url, @Nullable String urlParameters)
383 throws CommunicationApiException, UnauthorizedApiException {
384 String location = null;
385 if (urlParameters != null) {
386 location = url + "?" + urlParameters;
390 ContentResponse response = null;
391 int retriesLeft = Math.max(1, config.retry);
392 boolean connectionSuccess = false;
393 while (connectionSuccess == false && retriesLeft > 0) {
396 response = withGeneralProperties(httpClient.newRequest(location))
397 .timeout(config.timeout, TimeUnit.SECONDS).method(HttpMethod.GET).send();
398 connectionSuccess = true;
399 } catch (InterruptedException | TimeoutException | ExecutionException e) {
400 logger.warn("Request to OpenSprinkler device failed (retries left: {}): {}", retriesLeft,
404 if (connectionSuccess == false) {
405 throw new CommunicationApiException("Request to OpenSprinkler device failed");
407 if (response != null && response.getStatus() != HTTP_OK_CODE) {
408 throw new CommunicationApiException(
409 "Error sending HTTP GET request to " + url + ". Got response code: " + response.getStatus());
410 } else if (response != null) {
411 String content = response.getContentAsString();
412 if ("{\"result\":2}".equals(content)) {
413 throw new UnauthorizedApiException("Unauthorized, check your password is correct");
420 private Request withGeneralProperties(Request request) {
421 request.header(HttpHeader.USER_AGENT, USER_AGENT);
422 if (!config.basicUsername.isEmpty() && !config.basicPassword.isEmpty()) {
423 String encoded = Base64.getEncoder().encodeToString(
424 (config.basicUsername + ":" + config.basicPassword).getBytes(StandardCharsets.UTF_8));
425 request.header(HttpHeader.AUTHORIZATION, "Basic " + encoded);
431 * Given a URL and a set parameters, send a HTTP POST request to the URL
432 * location created by the URL and parameters.
434 * @param url The URL to send a POST request to.
435 * @param urlParameters List of parameters to use in the URL for the POST
436 * request. Null if no parameters.
437 * @return String contents of the response for the POST request.
440 public String sendHttpPost(String url, String urlParameters) throws CommunicationApiException {
441 ContentResponse response;
443 response = withGeneralProperties(httpClient.newRequest(url)).method(HttpMethod.POST)
444 .content(new StringContentProvider(urlParameters)).send();
445 } catch (InterruptedException | TimeoutException | ExecutionException e) {
446 throw new CommunicationApiException("Request to OpenSprinkler device failed: " + e.getMessage());
448 if (response.getStatus() != HTTP_OK_CODE) {
449 throw new CommunicationApiException(
450 "Error sending HTTP POST request to " + url + ". Got response code: " + response.getStatus());
452 return response.getContentAsString();
457 public int getQueuedZones() {
458 return state.jcReply.nq;
462 public int getCloudConnected() {
463 return state.jcReply.otcs;
467 public int getPausedState() {
468 return state.jcReply.pt;
472 public void setPausePrograms(int seconds) throws UnauthorizedApiException, CommunicationApiException {