]> git.basschouten.com Git - openhab-addons.git/blob
28227eef950a55ac9e6c1a010a2d7cf3d59efd85
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.OpenSprinklerBindingConstants.*;
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.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24 import java.util.stream.Collectors;
25
26 import javax.measure.quantity.Time;
27
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;
53
54 import com.google.gson.Gson;
55 import com.google.gson.JsonSyntaxException;
56
57 /**
58  * The {@link OpenSprinklerHttpApiV100} class is used for communicating with the
59  * OpenSprinkler API for firmware versions less than 2.1.0
60  *
61  * @author Chris Graham - Initial contribution
62  * @author Florian Schmidt - Allow https URLs and basic auth
63  */
64 @NonNullByDefault
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;
75
76     /**
77      * Constructor for the OpenSprinkler API class to create a connection to the
78      * OpenSprinkler device for control and obtaining status info.
79      *
80      * @param hostname Hostname or IP address as a String of the OpenSprinkler
81      *            device.
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
86      * @throws Exception
87      */
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;
93         } else {
94             this.hostname = HTTP_REQUEST_URL_PREFIX + config.hostname;
95         }
96         this.config = config;
97         this.password = config.password;
98         this.http = new HttpRequestSender(httpClient);
99     }
100
101     @Override
102     public boolean isManualModeEnabled() {
103         return isInManualMode;
104     }
105
106     @Override
107     public List<StateOption> getPrograms() {
108         return state.programs;
109     }
110
111     @Override
112     public List<StateOption> getStations() {
113         return state.stations;
114     }
115
116     @Override
117     public void refresh() throws CommunicationApiException, UnauthorizedApiException {
118         state.joReply = getOptions();
119         state.jsReply = getStationStatus();
120         state.jcReply = statusInfo();
121         state.jnReply = getStationNames();
122     }
123
124     @Override
125     public void enterManualMode() throws CommunicationApiException, UnauthorizedApiException {
126         http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_ENABLE_MANUAL_MODE);
127         numberOfStations = getNumberOfStations();
128         isInManualMode = true;
129     }
130
131     @Override
132     public void leaveManualMode() throws CommunicationApiException, UnauthorizedApiException {
133         http.sendHttpGet(getBaseUrl(), getRequestRequiredOptions() + "&" + CMD_DISABLE_MANUAL_MODE);
134         isInManualMode = false;
135     }
136
137     @Override
138     public void openStation(int station, BigDecimal duration) throws CommunicationApiException, GeneralApiException {
139         http.sendHttpGet(getBaseUrl() + "sn" + station + "=1&t=" + duration, null);
140     }
141
142     @Override
143     public void closeStation(int station) throws CommunicationApiException, GeneralApiException {
144         http.sendHttpGet(getBaseUrl() + "sn" + station + "=0", null);
145     }
146
147     @Override
148     public boolean isStationOpen(int station) throws CommunicationApiException, GeneralApiException {
149         String returnContent = http.sendHttpGet(getBaseUrl() + "sn" + station, null);
150         return "1".equals(returnContent);
151     }
152
153     @Override
154     public void ignoreRain(int station, boolean command) throws CommunicationApiException, UnauthorizedApiException {
155     }
156
157     @Override
158     public boolean isIgnoringRain(int station) {
159         return false;
160     }
161
162     @Override
163     public boolean isRainDetected() {
164         return state.jcReply.rs == 1;
165     }
166
167     @Override
168     public int getSensor2State() {
169         return state.jcReply.sn2;
170     }
171
172     @Override
173     public int currentDraw() {
174         return state.jcReply.curr;
175     }
176
177     @Override
178     public int flowSensorCount() {
179         return state.jcReply.flcrt;
180     }
181
182     @Override
183     public int signalStrength() {
184         return state.jcReply.rssi;
185     }
186
187     @Override
188     public boolean getIsEnabled() {
189         return state.jcReply.en == 1;
190     }
191
192     @Override
193     public int waterLevel() {
194         return state.joReply.wl;
195     }
196
197     @Override
198     public int getNumberOfStations() {
199         numberOfStations = state.jsReply.nstations;
200         return numberOfStations;
201     }
202
203     @Override
204     public int getFirmwareVersion() throws CommunicationApiException, UnauthorizedApiException {
205         state.joReply = getOptions();
206         return state.joReply.fwv;
207     }
208
209     @Override
210     public void runProgram(Command command) throws CommunicationApiException, UnauthorizedApiException {
211         logger.warn("OpenSprinkler requires at least firmware 217 for the runProgram feature to work");
212     }
213
214     @Override
215     public void enablePrograms(Command command) throws UnauthorizedApiException, CommunicationApiException {
216         if (command == OnOffType.ON) {
217             http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&en=1");
218         } else {
219             http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&en=0");
220         }
221     }
222
223     @Override
224     public void resetStations() throws UnauthorizedApiException, CommunicationApiException {
225         http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&rsn=1");
226     }
227
228     @Override
229     public void setRainDelay(int hours) throws UnauthorizedApiException, CommunicationApiException {
230         http.sendHttpGet(getBaseUrl() + "cv", getRequestRequiredOptions() + "&rd=" + hours);
231     }
232
233     @Override
234     public QuantityType<Time> getRainDelay() {
235         if (state.jcReply.rdst == 0) {
236             return new QuantityType<>(0, Units.SECOND);
237         }
238         long remainingTime = state.jcReply.rdst - state.jcReply.devt;
239         return new QuantityType<>(remainingTime, Units.SECOND);
240     }
241
242     /**
243      * Returns the hostname and port formatted URL as a String.
244      *
245      * @return String representation of the OpenSprinkler API URL.
246      */
247     protected String getBaseUrl() {
248         return hostname + ":" + config.port + "/";
249     }
250
251     /**
252      * Returns the required URL parameters required for every API call.
253      *
254      * @return String representation of the parameters needed during an API call.
255      */
256     protected String getRequestRequiredOptions() {
257         return CMD_PASSWORD + this.password;
258     }
259
260     @Override
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);
265         }
266         return new StationProgram(0);
267     }
268
269     private JcResponse statusInfo() throws CommunicationApiException, UnauthorizedApiException {
270         String returnContent;
271         JcResponse resp;
272         try {
273             returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATUS_INFO, getRequestRequiredOptions());
274             resp = gson.fromJson(returnContent, JcResponse.class);
275             if (resp == null) {
276                 throw new CommunicationApiException(
277                         "There was a problem in the HTTP communication: jcReply was empty.");
278             }
279         } catch (JsonSyntaxException exp) {
280             throw new CommunicationApiException(
281                     "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
282                             + exp.getMessage());
283         }
284         return resp;
285     }
286
287     private JoResponse getOptions() throws CommunicationApiException, UnauthorizedApiException {
288         String returnContent;
289         JoResponse resp;
290         try {
291             returnContent = http.sendHttpGet(getBaseUrl() + CMD_OPTIONS_INFO, getRequestRequiredOptions());
292             resp = gson.fromJson(returnContent, JoResponse.class);
293             if (resp == null) {
294                 throw new CommunicationApiException(
295                         "There was a problem in the HTTP communication: joReply was empty.");
296             }
297         } catch (JsonSyntaxException exp) {
298             throw new CommunicationApiException(
299                     "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
300                             + exp.getMessage());
301         }
302         return resp;
303     }
304
305     protected JsResponse getStationStatus() throws CommunicationApiException, UnauthorizedApiException {
306         String returnContent;
307         JsResponse resp;
308         try {
309             returnContent = http.sendHttpGet(getBaseUrl() + CMD_STATION_INFO, getRequestRequiredOptions());
310             resp = gson.fromJson(returnContent, JsResponse.class);
311             if (resp == null) {
312                 throw new CommunicationApiException(
313                         "There was a problem in the HTTP communication: jsReply was empty.");
314             }
315         } catch (JsonSyntaxException exp) {
316             throw new CommunicationApiException(
317                     "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
318                             + exp.getMessage());
319         }
320         return resp;
321     }
322
323     @Override
324     public void getProgramData() throws CommunicationApiException, UnauthorizedApiException {
325     }
326
327     @Override
328     public JnResponse getStationNames() throws CommunicationApiException, UnauthorizedApiException {
329         String returnContent;
330         JnResponse resp;
331         try {
332             returnContent = http.sendHttpGet(getBaseUrl() + "jn", getRequestRequiredOptions());
333             resp = gson.fromJson(returnContent, JnResponse.class);
334             if (resp == null) {
335                 throw new CommunicationApiException(
336                         "There was a problem in the HTTP communication: jnReply was empty.");
337             }
338         } catch (JsonSyntaxException exp) {
339             throw new CommunicationApiException(
340                     "There was a JSON syntax problem in the HTTP communication with the OpenSprinkler API: "
341                             + exp.getMessage());
342         }
343         state.jnReply = resp;
344         return resp;
345     }
346
347     /**
348      * This class contains helper methods for communicating HTTP GET and HTTP POST
349      * requests.
350      *
351      * @author Chris Graham - Initial contribution
352      * @author Florian Schmidt - Reduce visibility of Http communication to Api
353      */
354     protected class HttpRequestSender {
355         private static final int HTTP_OK_CODE = 200;
356         private static final String USER_AGENT = "Mozilla/5.0";
357
358         private final HttpClient httpClient;
359
360         public HttpRequestSender(HttpClient httpClient) {
361             this.httpClient = httpClient;
362         }
363
364         /**
365          * Given a URL and a set parameters, send a HTTP GET request to the URL location
366          * created by the URL and parameters.
367          *
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.
372          * @throws Exception
373          */
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;
379             } else {
380                 location = url;
381             }
382             ContentResponse response = null;
383             int retriesLeft = Math.max(1, config.retry);
384             boolean connectionSuccess = false;
385             while (connectionSuccess == false && retriesLeft > 0) {
386                 retriesLeft--;
387                 try {
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,
393                             e.getMessage());
394                 }
395             }
396             if (connectionSuccess == false) {
397                 throw new CommunicationApiException("Request to OpenSprinkler device failed");
398             }
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");
406                 }
407                 return content;
408             }
409             return "";
410         }
411
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);
418             }
419             return request;
420         }
421
422         /**
423          * Given a URL and a set parameters, send a HTTP POST request to the URL
424          * location created by the URL and parameters.
425          *
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.
430          * @throws Exception
431          */
432         public String sendHttpPost(String url, String urlParameters) throws CommunicationApiException {
433             ContentResponse response;
434             try {
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());
439             }
440             if (response.getStatus() != HTTP_OK_CODE) {
441                 throw new CommunicationApiException(
442                         "Error sending HTTP POST request to " + url + ". Got response code: " + response.getStatus());
443             }
444             return response.getContentAsString();
445         }
446     }
447
448     @Override
449     public int getQueuedZones() {
450         return state.jcReply.nq;
451     }
452
453     @Override
454     public int getCloudConnected() {
455         return state.jcReply.otcs;
456     }
457
458     @Override
459     public int getPausedState() {
460         return state.jcReply.pt;
461     }
462
463     @Override
464     public void setPausePrograms(int seconds) throws UnauthorizedApiException, CommunicationApiException {
465     }
466 }