]> git.basschouten.com Git - openhab-addons.git/blob
a19b9f5bdec65672057d7ba2a1297bcc3fd3c762
[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.robonect.internal;
14
15 import java.net.URI;
16 import java.nio.charset.StandardCharsets;
17 import java.util.Base64;
18 import java.util.concurrent.ExecutionException;
19 import java.util.concurrent.TimeUnit;
20 import java.util.concurrent.TimeoutException;
21
22 import org.eclipse.jetty.client.HttpClient;
23 import org.eclipse.jetty.client.api.Authentication;
24 import org.eclipse.jetty.client.api.AuthenticationStore;
25 import org.eclipse.jetty.client.api.ContentResponse;
26 import org.eclipse.jetty.client.api.Request;
27 import org.eclipse.jetty.http.HttpHeader;
28 import org.eclipse.jetty.http.HttpMethod;
29 import org.openhab.binding.robonect.internal.model.ErrorList;
30 import org.openhab.binding.robonect.internal.model.ModelParser;
31 import org.openhab.binding.robonect.internal.model.MowerInfo;
32 import org.openhab.binding.robonect.internal.model.MowerMode;
33 import org.openhab.binding.robonect.internal.model.Name;
34 import org.openhab.binding.robonect.internal.model.RobonectAnswer;
35 import org.openhab.binding.robonect.internal.model.VersionInfo;
36 import org.openhab.binding.robonect.internal.model.cmd.Command;
37 import org.openhab.binding.robonect.internal.model.cmd.ErrorCommand;
38 import org.openhab.binding.robonect.internal.model.cmd.ModeCommand;
39 import org.openhab.binding.robonect.internal.model.cmd.NameCommand;
40 import org.openhab.binding.robonect.internal.model.cmd.StartCommand;
41 import org.openhab.binding.robonect.internal.model.cmd.StatusCommand;
42 import org.openhab.binding.robonect.internal.model.cmd.StopCommand;
43 import org.openhab.binding.robonect.internal.model.cmd.VersionCommand;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48  * The {@link RobonectClient} class is responsible to communicate with the robonect module via it's HTTP interface.
49  *
50  * The API of the module is documented here: http://robonect.de/viewtopic.php?f=10&t=37
51  *
52  * @author Marco Meyer - Initial contribution
53  */
54 public class RobonectClient {
55
56     private final Logger logger = LoggerFactory.getLogger(RobonectClient.class);
57
58     private final String baseUrl;
59
60     private final HttpClient httpClient;
61
62     private final ModelParser parser;
63
64     private boolean jobRunning;
65
66     /**
67      * The {@link JobSettings} class holds the values required for starting a job.
68      */
69     public static class JobSettings {
70
71         private static final String TIME_REGEX = "^[012]\\d:\\d\\d$";
72
73         private final Logger logger = LoggerFactory.getLogger(RobonectClient.class);
74
75         private ModeCommand.RemoteStart remoteStart;
76         private ModeCommand.Mode after;
77         private int duration;
78
79         /**
80          * returns the 'remote start' setting for the job. See {@link ModeCommand.RemoteStart} for details.
81          *
82          * @return - the remote start settings for the job.
83          */
84         public ModeCommand.RemoteStart getRemoteStart() {
85             if (remoteStart != null) {
86                 return remoteStart;
87             } else {
88                 logger.debug("No explicit remote start set. Return STANDARD.");
89                 return ModeCommand.RemoteStart.STANDARD;
90             }
91         }
92
93         /**
94          * Sets the desired 'remote start' settings for the job.
95          *
96          * @param remoteStart - The 'remote start' settings. See {@link ModeCommand.RemoteStart} for the allowed modes.
97          */
98         public JobSettings withRemoteStart(ModeCommand.RemoteStart remoteStart) {
99             this.remoteStart = remoteStart;
100             return this;
101         }
102
103         /**
104          * Returns the mode the mower should be set to after the job is complete.
105          *
106          * @return - the mode after compleness of the job.
107          */
108         public ModeCommand.Mode getAfterMode() {
109             return after;
110         }
111
112         /**
113          * Sets the mode after the mower is complete with the job.
114          *
115          * @param after - the desired mode after job completeness.
116          */
117         public JobSettings withAfterMode(ModeCommand.Mode after) {
118             this.after = after;
119             return this;
120         }
121
122         public int getDuration() {
123             return duration;
124         }
125
126         public JobSettings withDuration(int duration) {
127             this.duration = duration;
128             return this;
129         }
130     }
131
132     private static class BasicResult implements Authentication.Result {
133
134         private final HttpHeader header;
135         private final URI uri;
136         private final String value;
137
138         public BasicResult(HttpHeader header, URI uri, String value) {
139             this.header = header;
140             this.uri = uri;
141             this.value = value;
142         }
143
144         @Override
145         public URI getURI() {
146             return this.uri;
147         }
148
149         @Override
150         public void apply(Request request) {
151             request.header(this.header, this.value);
152         }
153
154         @Override
155         public String toString() {
156             return String.format("Basic authentication result for %s", this.uri);
157         }
158     }
159
160     /**
161      * Creates an instance of RobonectClient which allows to communicate with the specified endpoint via the passed
162      * httpClient instance.
163      *
164      * @param httpClient - The HttpClient to use for the communication.
165      * @param endpoint - The endpoint information for connecting and issuing commands.
166      */
167     public RobonectClient(HttpClient httpClient, RobonectEndpoint endpoint) {
168         this.httpClient = httpClient;
169         this.baseUrl = "http://" + endpoint.getIpAddress() + "/json";
170         this.parser = new ModelParser();
171
172         if (endpoint.isUseAuthentication()) {
173             addPreemptiveAuthentication(httpClient, endpoint);
174         }
175     }
176
177     private void addPreemptiveAuthentication(HttpClient httpClient, RobonectEndpoint endpoint) {
178         AuthenticationStore auth = httpClient.getAuthenticationStore();
179         URI uri = URI.create(baseUrl);
180         auth.addAuthenticationResult(
181                 new BasicResult(HttpHeader.AUTHORIZATION, uri, "Basic " + Base64.getEncoder().encodeToString(
182                         (endpoint.getUser() + ":" + endpoint.getPassword()).getBytes(StandardCharsets.ISO_8859_1))));
183     }
184
185     /**
186      * returns general mower information. See {@MowerInfo} for the detailed information.
187      *
188      * @return - the general mower information including a general success status.
189      */
190     public MowerInfo getMowerInfo() {
191         String responseString = sendCommand(new StatusCommand());
192         MowerInfo mowerInfo = parser.parse(responseString, MowerInfo.class);
193         if (jobRunning) {
194             // mode might have been changed on the mower. Also Mode JOB does not really exist on the mower, thus cannot
195             // be checked here
196             if (mowerInfo.getStatus().getMode() == MowerMode.AUTO
197                     || mowerInfo.getStatus().getMode() == MowerMode.HOME) {
198                 jobRunning = false;
199             } else if (mowerInfo.getError() != null) {
200                 jobRunning = false;
201             }
202         }
203         return mowerInfo;
204     }
205
206     /**
207      * sends a start command to the mower.
208      *
209      * @return - a general answer with success status.
210      */
211     public RobonectAnswer start() {
212         String responseString = sendCommand(new StartCommand());
213         return parser.parse(responseString, RobonectAnswer.class);
214     }
215
216     /**
217      * sends a stop command to the mower.
218      *
219      * @return - a general answer with success status.
220      */
221     public RobonectAnswer stop() {
222         String responseString = sendCommand(new StopCommand());
223         return parser.parse(responseString, RobonectAnswer.class);
224     }
225
226     /**
227      * resets the errors on the mower.
228      *
229      * @return - a general answer with success status.
230      */
231     public RobonectAnswer resetErrors() {
232         String responseString = sendCommand(new ErrorCommand().withReset(true));
233         return parser.parse(responseString, RobonectAnswer.class);
234     }
235
236     /**
237      * returns the list of all errors happened since last reset.
238      *
239      * @return - the list of errors.
240      */
241     public ErrorList errorList() {
242         String responseString = sendCommand(new ErrorCommand());
243         return parser.parse(responseString, ErrorList.class);
244     }
245
246     /**
247      * Sets the mode of the mower. See {@link ModeCommand.Mode} for details about the available modes. Not allowed is
248      * mode
249      * {@link ModeCommand.Mode#JOB}.
250      *
251      * @param mode - the desired mower mode.
252      * @return - a general answer with success status.
253      */
254     public RobonectAnswer setMode(ModeCommand.Mode mode) {
255         String responseString = sendCommand(createCommand(mode));
256         if (jobRunning) {
257             jobRunning = false;
258         }
259         return parser.parse(responseString, RobonectAnswer.class);
260     }
261
262     private ModeCommand createCommand(ModeCommand.Mode mode) {
263         return new ModeCommand(mode);
264     }
265
266     /**
267      * Returns the name of the mower.
268      *
269      * @return - The name including a general answer with success status.
270      */
271     public Name getName() {
272         String responseString = sendCommand(new NameCommand());
273         return parser.parse(responseString, Name.class);
274     }
275
276     /**
277      * Allows to set the name of the mower.
278      *
279      * @param name - the desired name.
280      * @return - The resulting name including a general answer with success status.
281      */
282     public Name setName(String name) {
283         String responseString = sendCommand(new NameCommand().withNewName(name));
284         return parser.parse(responseString, Name.class);
285     }
286
287     private String sendCommand(Command command) {
288         try {
289             if (logger.isDebugEnabled()) {
290                 logger.debug("send HTTP GET to: {} ", command.toCommandURL(baseUrl));
291             }
292             ContentResponse response = httpClient.newRequest(command.toCommandURL(baseUrl)).method(HttpMethod.GET)
293                     .timeout(30000, TimeUnit.MILLISECONDS).send();
294             String responseString = null;
295
296             // jetty uses UTF-8 as default encoding. However, HTTP 1.1 specifies ISO_8859_1
297             if (response.getEncoding() == null || response.getEncoding().isBlank()) {
298                 responseString = new String(response.getContent(), StandardCharsets.ISO_8859_1);
299             } else {
300                 // currently v0.9e Robonect does not specifiy the encoding. But if later versions will
301                 // add, it should work with the default method to get the content as string.
302                 responseString = response.getContentAsString();
303             }
304
305             if (logger.isDebugEnabled()) {
306                 logger.debug("Response body was: {} ", responseString);
307             }
308             return responseString;
309         } catch (ExecutionException | TimeoutException | InterruptedException e) {
310             throw new RobonectCommunicationException("Could not send command " + command.toCommandURL(baseUrl), e);
311         }
312     }
313
314     /**
315      * Retrieve the version information of the mower and module. See {@link VersionInfo} for details.
316      *
317      * @return - the Version Information including the successful status.
318      */
319     public VersionInfo getVersionInfo() {
320         String versionResponse = sendCommand(new VersionCommand());
321         return parser.parse(versionResponse, VersionInfo.class);
322     }
323
324     public boolean isJobRunning() {
325         return jobRunning;
326     }
327
328     public RobonectAnswer startJob(JobSettings settings) {
329         Command jobCommand = new ModeCommand(ModeCommand.Mode.JOB).withRemoteStart(settings.remoteStart)
330                 .withAfter(settings.after).withDuration(settings.duration);
331         String responseString = sendCommand(jobCommand);
332         RobonectAnswer answer = parser.parse(responseString, RobonectAnswer.class);
333         if (answer.isSuccessful()) {
334             jobRunning = true;
335         } else {
336             jobRunning = false;
337         }
338         return answer;
339     }
340
341     public RobonectAnswer stopJob(JobSettings settings) {
342         RobonectAnswer answer = null;
343         if (jobRunning) {
344             answer = setMode(settings.after);
345             if (answer.isSuccessful()) {
346                 jobRunning = false;
347             }
348         } else {
349             answer = new RobonectAnswer();
350             // this is not an error, thus return success
351             answer.setSuccessful(true);
352         }
353         return answer;
354     }
355 }