]> git.basschouten.com Git - openhab-addons.git/blob
b282523f702ce4ac8dd10c18b85fa5dd636f1267
[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.*;
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<Time>(0, Units.SECOND);
237         }
238         long remainingTime = state.jcReply.rdst - state.jcReply.devt;
239         return new QuantityType<Time>(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;
383             try {
384                 response = withGeneralProperties(httpClient.newRequest(location)).timeout(5, TimeUnit.SECONDS)
385                         .method(HttpMethod.GET).send();
386             } catch (InterruptedException | TimeoutException | ExecutionException e) {
387                 throw new CommunicationApiException("Request to OpenSprinkler device failed: " + e.getMessage());
388             }
389             if (response.getStatus() != HTTP_OK_CODE) {
390                 throw new CommunicationApiException(
391                         "Error sending HTTP GET request to " + url + ". Got response code: " + response.getStatus());
392             }
393             String content = response.getContentAsString();
394             if ("{\"result\":2}".equals(content)) {
395                 throw new UnauthorizedApiException("Unauthorized, check your password is correct");
396             }
397             return content;
398         }
399
400         private Request withGeneralProperties(Request request) {
401             request.header(HttpHeader.USER_AGENT, USER_AGENT);
402             if (!config.basicUsername.isEmpty() && !config.basicPassword.isEmpty()) {
403                 String encoded = Base64.getEncoder().encodeToString(
404                         (config.basicUsername + ":" + config.basicPassword).getBytes(StandardCharsets.UTF_8));
405                 request.header(HttpHeader.AUTHORIZATION, "Basic " + encoded);
406             }
407             return request;
408         }
409
410         /**
411          * Given a URL and a set parameters, send a HTTP POST request to the URL
412          * location created by the URL and parameters.
413          *
414          * @param url The URL to send a POST request to.
415          * @param urlParameters List of parameters to use in the URL for the POST
416          *            request. Null if no parameters.
417          * @return String contents of the response for the POST request.
418          * @throws Exception
419          */
420         public String sendHttpPost(String url, String urlParameters) throws CommunicationApiException {
421             ContentResponse response;
422             try {
423                 response = withGeneralProperties(httpClient.newRequest(url)).method(HttpMethod.POST)
424                         .content(new StringContentProvider(urlParameters)).send();
425             } catch (InterruptedException | TimeoutException | ExecutionException e) {
426                 throw new CommunicationApiException("Request to OpenSprinkler device failed: " + e.getMessage());
427             }
428             if (response.getStatus() != HTTP_OK_CODE) {
429                 throw new CommunicationApiException(
430                         "Error sending HTTP POST request to " + url + ". Got response code: " + response.getStatus());
431             }
432             return response.getContentAsString();
433         }
434     }
435
436     @Override
437     public int getQueuedZones() {
438         return state.jcReply.nq;
439     }
440
441     @Override
442     public int getCloudConnected() {
443         return state.jcReply.otcs;
444     }
445
446     @Override
447     public int getPausedState() {
448         return state.jcReply.pt;
449     }
450
451     @Override
452     public void setPausePrograms(int seconds) throws UnauthorizedApiException, CommunicationApiException {
453     }
454 }