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