2 * Copyright (c) 2010-2024 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.*;
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.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24 import java.util.stream.Collectors;
26 import javax.measure.quantity.Time;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.eclipse.jetty.client.api.ContentResponse;
32 import org.eclipse.jetty.client.api.Request;
33 import org.eclipse.jetty.client.util.StringContentProvider;
34 import org.eclipse.jetty.http.HttpHeader;
35 import org.eclipse.jetty.http.HttpMethod;
36 import org.openhab.binding.opensprinkler.internal.OpenSprinklerState;
37 import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JcResponse;
38 import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JnResponse;
39 import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JoResponse;
40 import org.openhab.binding.opensprinkler.internal.OpenSprinklerState.JsResponse;
41 import org.openhab.binding.opensprinkler.internal.api.exception.CommunicationApiException;
42 import org.openhab.binding.opensprinkler.internal.api.exception.GeneralApiException;
43 import org.openhab.binding.opensprinkler.internal.api.exception.UnauthorizedApiException;
44 import org.openhab.binding.opensprinkler.internal.config.OpenSprinklerHttpInterfaceConfig;
45 import org.openhab.binding.opensprinkler.internal.model.StationProgram;
46 import org.openhab.core.library.types.OnOffType;
47 import org.openhab.core.library.types.QuantityType;
48 import org.openhab.core.library.unit.Units;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.StateOption;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
54 import com.google.gson.Gson;
55 import com.google.gson.JsonSyntaxException;
58 * The {@link OpenSprinklerHttpApiV100} class is used for communicating with the
59 * OpenSprinkler API for firmware versions less than 2.1.0
61 * @author Chris Graham - Initial contribution
62 * @author Florian Schmidt - Allow https URLs and basic auth
65 class OpenSprinklerHttpApiV100 implements OpenSprinklerApi {
66 protected final Logger logger = LoggerFactory.getLogger(this.getClass());
67 protected final String hostname;
68 protected final OpenSprinklerHttpInterfaceConfig config;
69 protected String password;
70 protected OpenSprinklerState state = new OpenSprinklerState();
71 protected int numberOfStations = DEFAULT_STATION_COUNT;
72 protected boolean isInManualMode = false;
73 protected final Gson gson = new Gson();
74 protected HttpRequestSender http;
77 * Constructor for the OpenSprinkler API class to create a connection to the
78 * OpenSprinkler device for control and obtaining status info.
80 * @param hostname Hostname or IP address as a String of the OpenSprinkler
82 * @param port The port number the OpenSprinkler API is listening on.
83 * @param password Admin password for the OpenSprinkler device.
84 * @param basicUsername only needed if basic auth is required
85 * @param basicPassword only needed if basic auth is required
88 OpenSprinklerHttpApiV100(final HttpClient httpClient, final OpenSprinklerHttpInterfaceConfig config)
89 throws CommunicationApiException, UnauthorizedApiException {
90 if (config.hostname.startsWith(HTTP_REQUEST_URL_PREFIX)
91 || config.hostname.startsWith(HTTPS_REQUEST_URL_PREFIX)) {
92 this.hostname = config.hostname;
94 this.hostname = HTTP_REQUEST_URL_PREFIX + config.hostname;
97 this.password = config.password;
98 this.http = new HttpRequestSender(httpClient);
102 public boolean isManualModeEnabled() {
103 return isInManualMode;
107 public List<StateOption> getPrograms() {
108 return state.programs;
112 public List<StateOption> getStations() {
113 return state.stations;
117 public void refresh() throws CommunicationApiException, UnauthorizedApiException {
118 state.joReply = getOptions();
119 state.jsReply = getStationStatus();
120 state.jcReply = statusInfo();
121 state.jnReply = getStationNames();
125 public void enterManualMode() throws CommunicationApiException, UnauthorizedApiException {
126 http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_ENABLE_MANUAL_MODE);
127 numberOfStations = getNumberOfStations();
128 isInManualMode = true;
132 public void leaveManualMode() throws CommunicationApiException, UnauthorizedApiException {
133 http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_DISABLE_MANUAL_MODE);
134 isInManualMode = false;
138 public void openStation(int station, BigDecimal duration) throws CommunicationApiException, GeneralApiException {
139 http.sendHttpGet(getBaseUrl() + "sn" + station + "=1&t=" + duration, null);
143 public void closeStation(int station) throws CommunicationApiException, GeneralApiException {
144 http.sendHttpGet(getBaseUrl() + "sn" + station + "=0", null);
148 public boolean isStationOpen(int station) throws CommunicationApiException, GeneralApiException {
149 String returnContent = http.sendHttpGet(getBaseUrl() + "sn" + station, null);
150 return "1".equals(returnContent);
154 public void ignoreRain(int station, boolean command) throws CommunicationApiException, UnauthorizedApiException {
158 public boolean isIgnoringRain(int station) {
163 public boolean isRainDetected() {
164 return state.jcReply.rs == 1;
168 public int getSensor2State() {
169 return state.jcReply.sn2;
173 public int currentDraw() {
174 return state.jcReply.curr;
178 public int flowSensorCount() {
179 return state.jcReply.flcrt;
183 public int signalStrength() {
184 return state.jcReply.rssi;
188 public boolean getIsEnabled() {
189 return state.jcReply.en == 1;
193 public int waterLevel() {
194 return state.joReply.wl;
198 public int getNumberOfStations() {
199 numberOfStations = state.jsReply.nstations;
200 return numberOfStations;
204 public int getFirmwareVersion() throws CommunicationApiException, UnauthorizedApiException {
205 state.joReply = getOptions();
206 return state.joReply.fwv;
210 public void runProgram(Command command) throws CommunicationApiException, UnauthorizedApiException {
211 logger.warn("OpenSprinkler requires at least firmware 217 for the runProgram feature to work");
215 public void enablePrograms(Command command) throws UnauthorizedApiException, CommunicationApiException {
216 if (command == OnOffType.ON) {
217 http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&en=1");
219 http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&en=0");
224 public void resetStations() throws UnauthorizedApiException, CommunicationApiException {
225 http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&rsn=1");
229 public void setRainDelay(int hours) throws UnauthorizedApiException, CommunicationApiException {
230 http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&rd=" + hours);
234 public QuantityType<Time> getRainDelay() {
235 if (state.jcReply.rdst == 0) {
236 return new QuantityType<>(0, Units.SECOND);
238 long remainingTime = state.jcReply.rdst - state.jcReply.devt;
239 return new QuantityType<>(remainingTime, Units.SECOND);
243 * Returns the hostname and port formatted URL as a String.
245 * @return String representation of the OpenSprinkler API URL.
247 protected String getBaseUrl() {
248 return hostname + ":" + config.port + "/";
252 * Returns the required URL parameters required for every API call.
254 * @return String representation of the parameters needed during an API call.
256 protected String getRequestRequiredOptions() {
257 return CMD_PASSWORD + this.password;
261 public StationProgram retrieveProgram(int station) throws CommunicationApiException {
262 if (state.jcReply.ps != null) {
263 return state.jcReply.ps.stream().map(values -> new StationProgram(values.get(1)))
264 .collect(Collectors.toList()).get(station);
266 return new StationProgram(0);
269 private JcResponse statusInfo() throws CommunicationApiException, UnauthorizedApiException {
270 String returnContent;
273 returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATUS_INFO, getRequestRequiredOptions());
274 resp = gson.fromJson(returnContent, JcResponse.class);
276 throw new CommunicationApiException(
277 "There was a problem in the HTTP communication: jcReply was empty.");
279 } catch (JsonSyntaxException exp) {
280 throw new CommunicationApiException(
281 "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
287 private JoResponse getOptions() throws CommunicationApiException, UnauthorizedApiException {
288 String returnContent;
291 returnContent = http.sendHttpGet(getBaseUrl() + CMD_OPTIONS_INFO, getRequestRequiredOptions());
292 resp = gson.fromJson(returnContent, JoResponse.class);
294 throw new CommunicationApiException(
295 "There was a problem in the HTTP communication: joReply was empty.");
297 } catch (JsonSyntaxException exp) {
298 throw new CommunicationApiException(
299 "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
305 protected JsResponse getStationStatus() throws CommunicationApiException, UnauthorizedApiException {
306 String returnContent;
309 returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATION_INFO, getRequestRequiredOptions());
310 resp = gson.fromJson(returnContent, JsResponse.class);
312 throw new CommunicationApiException(
313 "There was a problem in the HTTP communication: jsReply was empty.");
315 } catch (JsonSyntaxException exp) {
316 throw new CommunicationApiException(
317 "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
324 public void getProgramData() throws CommunicationApiException, UnauthorizedApiException {
328 public JnResponse getStationNames() throws CommunicationApiException, UnauthorizedApiException {
329 String returnContent;
332 returnContent = http.sendHttpGet(getBaseUrl() + "jn", getRequestRequiredOptions());
333 resp = gson.fromJson(returnContent, JnResponse.class);
335 throw new CommunicationApiException(
336 "There was a problem in the HTTP communication: jnReply was empty.");
338 } catch (JsonSyntaxException exp) {
339 throw new CommunicationApiException(
340 "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
343 state.jnReply = resp;
348 * This class contains helper methods for communicating HTTP GET and HTTP POST
351 * @author Chris Graham - Initial contribution
352 * @author Florian Schmidt - Reduce visibility of Http communication to Api
354 protected class HttpRequestSender {
355 private static final int HTTP_OK_CODE = 200;
356 private static final String USER_AGENT = "Mozilla/5.0";
358 private final HttpClient httpClient;
360 public HttpRequestSender(HttpClient httpClient) {
361 this.httpClient = httpClient;
365 * Given a URL and a set parameters, send a HTTP GET request to the URL location
366 * created by the URL and parameters.
368 * @param url The URL to send a GET request to.
369 * @param urlParameters List of parameters to use in the URL for the GET
370 * request. Null if no parameters.
371 * @return String contents of the response for the GET request.
374 public String sendHttpGet(String url, @Nullable String urlParameters)
375 throws CommunicationApiException, UnauthorizedApiException {
376 String location = null;
377 if (urlParameters != null) {
378 location = url + "?" + urlParameters;
382 ContentResponse response = null;
383 int retriesLeft = Math.max(1, config.retry);
384 boolean connectionSuccess = false;
385 while (connectionSuccess == false && retriesLeft > 0) {
388 response = withGeneralProperties(httpClient.newRequest(location))
389 .timeout(config.timeout, TimeUnit.SECONDS).method(HttpMethod.GET).send();
390 connectionSuccess = true;
391 } catch (InterruptedException | TimeoutException | ExecutionException e) {
392 logger.debug("Request to OpenSprinkler device failed (retries left: {}): {}", retriesLeft,
396 if (connectionSuccess == false) {
397 throw new CommunicationApiException("Request to OpenSprinkler device failed");
399 if (response != null && response.getStatus() != HTTP_OK_CODE) {
400 throw new CommunicationApiException(
401 "Error sending HTTP GET request to " + url + ". Got response code: " + response.getStatus());
402 } else if (response != null) {
403 String content = response.getContentAsString();
404 if ("{\"result\":2}".equals(content)) {
405 throw new UnauthorizedApiException("Unauthorized, check your password is correct");
412 private Request withGeneralProperties(Request request) {
413 request.header(HttpHeader.USER_AGENT, USER_AGENT);
414 if (!config.basicUsername.isEmpty() && !config.basicPassword.isEmpty()) {
415 String encoded = Base64.getEncoder().encodeToString(
416 (config.basicUsername + ":" + config.basicPassword).getBytes(StandardCharsets.UTF_8));
417 request.header(HttpHeader.AUTHORIZATION, "Basic " + encoded);
423 * Given a URL and a set parameters, send a HTTP POST request to the URL
424 * location created by the URL and parameters.
426 * @param url The URL to send a POST request to.
427 * @param urlParameters List of parameters to use in the URL for the POST
428 * request. Null if no parameters.
429 * @return String contents of the response for the POST request.
432 public String sendHttpPost(String url, String urlParameters) throws CommunicationApiException {
433 ContentResponse response;
435 response = withGeneralProperties(httpClient.newRequest(url)).method(HttpMethod.POST)
436 .content(new StringContentProvider(urlParameters)).send();
437 } catch (InterruptedException | TimeoutException | ExecutionException e) {
438 throw new CommunicationApiException("Request to OpenSprinkler device failed: " + e.getMessage());
440 if (response.getStatus() != HTTP_OK_CODE) {
441 throw new CommunicationApiException(
442 "Error sending HTTP POST request to " + url + ". Got response code: " + response.getStatus());
444 return response.getContentAsString();
449 public int getQueuedZones() {
450 return state.jcReply.nq;
454 public int getCloudConnected() {
455 return state.jcReply.otcs;
459 public int getPausedState() {
460 return state.jcReply.pt;
464 public void setPausePrograms(int seconds) throws UnauthorizedApiException, CommunicationApiException {