]> git.basschouten.com Git - openhab-addons.git/blob
f2164d98f41be318729cf823b46225bd53a73d98
[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  * @see <a href="http://robonect.de/viewtopic.php?f=10&t=37">
51  *      http://robonect.de/viewtopic.php?f=10&amp;t=37</a>. The API of the module is documented here.
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         @Override
146         public URI getURI() {
147             return this.uri;
148         }
149
150         @Override
151         public void apply(Request request) {
152             request.header(this.header, this.value);
153         }
154
155         @Override
156         public String toString() {
157             return String.format("Basic authentication result for %s", this.uri);
158         }
159     }
160
161     /**
162      * Creates an instance of RobonectClient which allows to communicate with the specified endpoint via the passed
163      * httpClient instance.
164      *
165      * @param httpClient - The HttpClient to use for the communication.
166      * @param endpoint - The endpoint information for connecting and issuing commands.
167      */
168     public RobonectClient(HttpClient httpClient, RobonectEndpoint endpoint) {
169         this.httpClient = httpClient;
170         this.baseUrl = "http://" + endpoint.getIpAddress() + "/json";
171         this.parser = new ModelParser();
172
173         if (endpoint.isUseAuthentication()) {
174             addPreemptiveAuthentication(httpClient, endpoint);
175         }
176     }
177
178     private void addPreemptiveAuthentication(HttpClient httpClient, RobonectEndpoint endpoint) {
179         AuthenticationStore auth = httpClient.getAuthenticationStore();
180         URI uri = URI.create(baseUrl);
181         auth.addAuthenticationResult(
182                 new BasicResult(HttpHeader.AUTHORIZATION, uri, "Basic " + Base64.getEncoder().encodeToString(
183                         (endpoint.getUser() + ":" + endpoint.getPassword()).getBytes(StandardCharsets.ISO_8859_1))));
184     }
185
186     /**
187      * returns general mower information. See {@link MowerInfo} for the detailed information.
188      *
189      * @return - the general mower information including a general success status.
190      */
191     public MowerInfo getMowerInfo() {
192         String responseString = sendCommand(new StatusCommand());
193         MowerInfo mowerInfo = parser.parse(responseString, MowerInfo.class);
194         if (jobRunning) {
195             // mode might have been changed on the mower. Also Mode JOB does not really exist on the mower, thus cannot
196             // be checked here
197             if (mowerInfo.getStatus().getMode() == MowerMode.AUTO
198                     || mowerInfo.getStatus().getMode() == MowerMode.HOME) {
199                 jobRunning = false;
200             } else if (mowerInfo.getError() != null) {
201                 jobRunning = false;
202             }
203         }
204         return mowerInfo;
205     }
206
207     /**
208      * sends a start command to the mower.
209      *
210      * @return - a general answer with success status.
211      */
212     public RobonectAnswer start() {
213         String responseString = sendCommand(new StartCommand());
214         return parser.parse(responseString, RobonectAnswer.class);
215     }
216
217     /**
218      * sends a stop command to the mower.
219      *
220      * @return - a general answer with success status.
221      */
222     public RobonectAnswer stop() {
223         String responseString = sendCommand(new StopCommand());
224         return parser.parse(responseString, RobonectAnswer.class);
225     }
226
227     /**
228      * resets the errors on the mower.
229      *
230      * @return - a general answer with success status.
231      */
232     public RobonectAnswer resetErrors() {
233         String responseString = sendCommand(new ErrorCommand().withReset(true));
234         return parser.parse(responseString, RobonectAnswer.class);
235     }
236
237     /**
238      * returns the list of all errors happened since last reset.
239      *
240      * @return - the list of errors.
241      */
242     public ErrorList errorList() {
243         String responseString = sendCommand(new ErrorCommand());
244         return parser.parse(responseString, ErrorList.class);
245     }
246
247     /**
248      * Sets the mode of the mower. See {@link ModeCommand.Mode} for details about the available modes. Not allowed is
249      * mode
250      * {@link ModeCommand.Mode#JOB}.
251      *
252      * @param mode - the desired mower mode.
253      * @return - a general answer with success status.
254      */
255     public RobonectAnswer setMode(ModeCommand.Mode mode) {
256         String responseString = sendCommand(createCommand(mode));
257         if (jobRunning) {
258             jobRunning = false;
259         }
260         return parser.parse(responseString, RobonectAnswer.class);
261     }
262
263     private ModeCommand createCommand(ModeCommand.Mode mode) {
264         return new ModeCommand(mode);
265     }
266
267     /**
268      * Returns the name of the mower.
269      *
270      * @return - The name including a general answer with success status.
271      */
272     public Name getName() {
273         String responseString = sendCommand(new NameCommand());
274         return parser.parse(responseString, Name.class);
275     }
276
277     /**
278      * Allows to set the name of the mower.
279      *
280      * @param name - the desired name.
281      * @return - The resulting name including a general answer with success status.
282      */
283     public Name setName(String name) {
284         String responseString = sendCommand(new NameCommand().withNewName(name));
285         return parser.parse(responseString, Name.class);
286     }
287
288     private String sendCommand(Command command) {
289         try {
290             if (logger.isDebugEnabled()) {
291                 logger.debug("send HTTP GET to: {} ", command.toCommandURL(baseUrl));
292             }
293             ContentResponse response = httpClient.newRequest(command.toCommandURL(baseUrl)).method(HttpMethod.GET)
294                     .timeout(30000, TimeUnit.MILLISECONDS).send();
295             String responseString = null;
296
297             // jetty uses UTF-8 as default encoding. However, HTTP 1.1 specifies ISO_8859_1
298             if (response.getEncoding() == null || response.getEncoding().isBlank()) {
299                 responseString = new String(response.getContent(), StandardCharsets.ISO_8859_1);
300             } else {
301                 // currently v0.9e Robonect does not specifiy the encoding. But if later versions will
302                 // add, it should work with the default method to get the content as string.
303                 responseString = response.getContentAsString();
304             }
305
306             if (logger.isDebugEnabled()) {
307                 logger.debug("Response body was: {} ", responseString);
308             }
309             return responseString;
310         } catch (ExecutionException | TimeoutException | InterruptedException e) {
311             throw new RobonectCommunicationException("Could not send command " + command.toCommandURL(baseUrl), e);
312         }
313     }
314
315     /**
316      * Retrieve the version information of the mower and module. See {@link VersionInfo} for details.
317      *
318      * @return - the Version Information including the successful status.
319      */
320     public VersionInfo getVersionInfo() {
321         String versionResponse = sendCommand(new VersionCommand());
322         return parser.parse(versionResponse, VersionInfo.class);
323     }
324
325     public boolean isJobRunning() {
326         return jobRunning;
327     }
328
329     public RobonectAnswer startJob(JobSettings settings) {
330         Command jobCommand = new ModeCommand(ModeCommand.Mode.JOB).withRemoteStart(settings.remoteStart)
331                 .withAfter(settings.after).withDuration(settings.duration);
332         String responseString = sendCommand(jobCommand);
333         RobonectAnswer answer = parser.parse(responseString, RobonectAnswer.class);
334         if (answer.isSuccessful()) {
335             jobRunning = true;
336         } else {
337             jobRunning = false;
338         }
339         return answer;
340     }
341
342     public RobonectAnswer stopJob(JobSettings settings) {
343         RobonectAnswer answer = null;
344         if (jobRunning) {
345             answer = setMode(settings.after);
346             if (answer.isSuccessful()) {
347                 jobRunning = false;
348             }
349         } else {
350             answer = new RobonectAnswer();
351             // this is not an error, thus return success
352             answer.setSuccessful(true);
353         }
354         return answer;
355     }
356 }