2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.robonect.internal;
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;
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;
48 * The {@link RobonectClient} class is responsible to communicate with the robonect module via it's HTTP interface.
50 * @see <a href="http://robonect.de/viewtopic.php?f=10&t=37">
51 * http://robonect.de/viewtopic.php?f=10&t=37</a>. The API of the module is documented here.
53 * @author Marco Meyer - Initial contribution
55 public class RobonectClient {
57 private final Logger logger = LoggerFactory.getLogger(RobonectClient.class);
59 private final String baseUrl;
61 private final HttpClient httpClient;
63 private final ModelParser parser;
65 private boolean jobRunning;
68 * The {@link JobSettings} class holds the values required for starting a job.
70 public static class JobSettings {
72 private static final String TIME_REGEX = "^[012]\\d:\\d\\d$";
74 private final Logger logger = LoggerFactory.getLogger(RobonectClient.class);
76 private ModeCommand.RemoteStart remoteStart;
77 private ModeCommand.Mode after;
81 * returns the 'remote start' setting for the job. See {@link ModeCommand.RemoteStart} for details.
83 * @return - the remote start settings for the job.
85 public ModeCommand.RemoteStart getRemoteStart() {
86 if (remoteStart != null) {
89 logger.debug("No explicit remote start set. Return STANDARD.");
90 return ModeCommand.RemoteStart.STANDARD;
95 * Sets the desired 'remote start' settings for the job.
97 * @param remoteStart - The 'remote start' settings. See {@link ModeCommand.RemoteStart} for the allowed modes.
99 public JobSettings withRemoteStart(ModeCommand.RemoteStart remoteStart) {
100 this.remoteStart = remoteStart;
105 * Returns the mode the mower should be set to after the job is complete.
107 * @return - the mode after compleness of the job.
109 public ModeCommand.Mode getAfterMode() {
114 * Sets the mode after the mower is complete with the job.
116 * @param after - the desired mode after job completeness.
118 public JobSettings withAfterMode(ModeCommand.Mode after) {
123 public int getDuration() {
127 public JobSettings withDuration(int duration) {
128 this.duration = duration;
133 private static class BasicResult implements Authentication.Result {
135 private final HttpHeader header;
136 private final URI uri;
137 private final String value;
139 public BasicResult(HttpHeader header, URI uri, String value) {
140 this.header = header;
146 public URI getURI() {
151 public void apply(Request request) {
152 request.header(this.header, this.value);
156 public String toString() {
157 return String.format("Basic authentication result for %s", this.uri);
162 * Creates an instance of RobonectClient which allows to communicate with the specified endpoint via the passed
163 * httpClient instance.
165 * @param httpClient - The HttpClient to use for the communication.
166 * @param endpoint - The endpoint information for connecting and issuing commands.
168 public RobonectClient(HttpClient httpClient, RobonectEndpoint endpoint) {
169 this.httpClient = httpClient;
170 this.baseUrl = "http://" + endpoint.getIpAddress() + "/json";
171 this.parser = new ModelParser();
173 if (endpoint.isUseAuthentication()) {
174 addPreemptiveAuthentication(httpClient, endpoint);
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))));
187 * returns general mower information. See {@link MowerInfo} for the detailed information.
189 * @return - the general mower information including a general success status.
191 public MowerInfo getMowerInfo() {
192 String responseString = sendCommand(new StatusCommand());
193 MowerInfo mowerInfo = parser.parse(responseString, MowerInfo.class);
195 // mode might have been changed on the mower. Also Mode JOB does not really exist on the mower, thus cannot
197 if (mowerInfo.getStatus().getMode() == MowerMode.AUTO
198 || mowerInfo.getStatus().getMode() == MowerMode.HOME) {
200 } else if (mowerInfo.getError() != null) {
208 * sends a start command to the mower.
210 * @return - a general answer with success status.
212 public RobonectAnswer start() {
213 String responseString = sendCommand(new StartCommand());
214 return parser.parse(responseString, RobonectAnswer.class);
218 * sends a stop command to the mower.
220 * @return - a general answer with success status.
222 public RobonectAnswer stop() {
223 String responseString = sendCommand(new StopCommand());
224 return parser.parse(responseString, RobonectAnswer.class);
228 * resets the errors on the mower.
230 * @return - a general answer with success status.
232 public RobonectAnswer resetErrors() {
233 String responseString = sendCommand(new ErrorCommand().withReset(true));
234 return parser.parse(responseString, RobonectAnswer.class);
238 * returns the list of all errors happened since last reset.
240 * @return - the list of errors.
242 public ErrorList errorList() {
243 String responseString = sendCommand(new ErrorCommand());
244 return parser.parse(responseString, ErrorList.class);
248 * Sets the mode of the mower. See {@link ModeCommand.Mode} for details about the available modes. Not allowed is
250 * {@link ModeCommand.Mode#JOB}.
252 * @param mode - the desired mower mode.
253 * @return - a general answer with success status.
255 public RobonectAnswer setMode(ModeCommand.Mode mode) {
256 String responseString = sendCommand(createCommand(mode));
260 return parser.parse(responseString, RobonectAnswer.class);
263 private ModeCommand createCommand(ModeCommand.Mode mode) {
264 return new ModeCommand(mode);
268 * Returns the name of the mower.
270 * @return - The name including a general answer with success status.
272 public Name getName() {
273 String responseString = sendCommand(new NameCommand());
274 return parser.parse(responseString, Name.class);
278 * Allows to set the name of the mower.
280 * @param name - the desired name.
281 * @return - The resulting name including a general answer with success status.
283 public Name setName(String name) {
284 String responseString = sendCommand(new NameCommand().withNewName(name));
285 return parser.parse(responseString, Name.class);
288 private String sendCommand(Command command) {
290 if (logger.isDebugEnabled()) {
291 logger.debug("send HTTP GET to: {} ", command.toCommandURL(baseUrl));
293 ContentResponse response = httpClient.newRequest(command.toCommandURL(baseUrl)).method(HttpMethod.GET)
294 .timeout(30000, TimeUnit.MILLISECONDS).send();
295 String responseString = null;
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);
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();
306 if (logger.isDebugEnabled()) {
307 logger.debug("Response body was: {} ", responseString);
309 return responseString;
310 } catch (ExecutionException | TimeoutException | InterruptedException e) {
311 throw new RobonectCommunicationException("Could not send command " + command.toCommandURL(baseUrl), e);
316 * Retrieve the version information of the mower and module. See {@link VersionInfo} for details.
318 * @return - the Version Information including the successful status.
320 public VersionInfo getVersionInfo() {
321 String versionResponse = sendCommand(new VersionCommand());
322 return parser.parse(versionResponse, VersionInfo.class);
325 public boolean isJobRunning() {
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()) {
342 public RobonectAnswer stopJob(JobSettings settings) {
343 RobonectAnswer answer = null;
345 answer = setMode(settings.after);
346 if (answer.isSuccessful()) {
350 answer = new RobonectAnswer();
351 // this is not an error, thus return success
352 answer.setSuccessful(true);