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