diff --git a/bundles/org.openhab.binding.boschindego/README.md b/bundles/org.openhab.binding.boschindego/README.md
index 5554ee336..a3aa3889e 100644
--- a/bundles/org.openhab.binding.boschindego/README.md
+++ b/bundles/org.openhab.binding.boschindego/README.md
@@ -1,69 +1,83 @@
# Bosch Indego Binding
This is the Binding for Bosch Indego Connect lawn mowers.
-Thank´s to zazaz-de who found out how the API works. His [Java Library](https://github.com/zazaz-de/iot-device-bosch-indego-controller) made this Binding possible.
+Thank´s to zazaz-de who found out how the API works.
+His [Java Library](https://github.com/zazaz-de/iot-device-bosch-indego-controller) made this Binding possible.
-## Configuration of the Thing
+## Thing Configuration
-Currently the binding supports ***indego*** mowers as a thing type with this parameters:
+Currently the binding supports ***indego*** mowers as a thing type with these configuration parameters:
-| parameter | datatype | required |
-|-----------|----------|--------------------------------|
-| username | String | yes |
-| password | String | yes |
-| refresh | integer | no (default: 180, minimum: 60) |
-
-The refresh interval is specified in seconds.
-
-A possible entry in your thing file could be:
-
-```java
-boschindego:indego:lawnmower [username="mail@example.com", password="idontneedtocutthelawnagain", refresh=120]
-```
+| Parameter | Description |
+|-----------|----------------------------------------------------------------------|
+| username | Username for the Bosch Indego account |
+| password | Password for the Bosch Indego account |
+| refresh | Specifies the refresh interval in seconds (default 180, minimum: 60) |
## Channels
-| item-type | description | |
+| Channel | Item Type | Description |
|--------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------|
| state | Number | You can send commands to this channel to control the mower and read the simplified state from it (1=mow, 2=return to dock, 3=pause) |
-| errorcode | Number | Errorcode of the mower (0=no error, readonly) |
-| statecode | Number | Detailed state of the mower. I included English and German map-files to read the state easier (readonly) |
+| errorcode | Number | Error code of the mower (0=no error, readonly) |
+| statecode | Number | Detailed state of the mower (readonly) |
| textualstate | String | State as a text. (readonly) |
| ready | Number | Shows if the mower is ready to mow (1=ready, 0=not ready, readonly) |
| mowed | Dimmer | Cut grass in percent (readonly) |
-For example you can use this sitemap entry to control the mower manually:
+### State Codes
-```perl
-Switch item=indegostate mappings=[ 1="Mow", 2="Return",3="Pause" ]
+| Code | Description |
+|-------|---------------------------------------------|
+| 0 | Reading status |
+| 257 | Charging |
+| 258 | Docked |
+| 259 | Docked - Software update |
+| 260 | Docked |
+| 261 | Docked |
+| 262 | Docked - Loading map |
+| 263 | Docked - Saving map |
+| 513 | Mowing |
+| 514 | Relocalising |
+| 515 | Loading map |
+| 516 | Learning lawn |
+| 517 | Paused |
+| 518 | Border cut |
+| 519 | Idle in lawn |
+| 769 | Returning to dock |
+| 770 | Returning to dock |
+| 771 | Returning to dock - Battery low |
+| 772 | Returning to dock - Calendar timeslot ended |
+| 773 | Returning to dock - Battery temp range |
+| 774 | Returning to dock |
+| 775 | Returning to dock - Lawn complete |
+| 776 | Returning to dock - Relocalising |
+| 1025 | Diagnostic mode |
+| 1026 | End of life |
+| 1281 | Software update |
+| 64513 | Docked |
+
+## Full Example
+
+### `indego.things` File
+
+```
+boschindego:indego:lawnmower [username="mail@example.com", password="idontneedtocutthelawnagain", refresh=120]
```
-## Meaning of the numeric statecodes
+### `indego.items` File
-You can use this as .map file
-
-```text
-0=Reading status
-257=Charging
-258=Docked
-259=Docked - Software update
-260=Docked
-261=Docked
-262=Docked - Loading map
-263=Docked - Saving map
-513=Mowing
-514=Relocalising
-515=Loading map
-516=Learning lawn
-517=Paused
-518=Border cut
-519=Idle in lawn
-769=Returning to Dock
-770=Returning to Dock
-771=Returning to Dock - Battery low
-772=Returning to dock - Calendar timeslot ended
-773=Returning to dock - Battery temp range
-774=Returning to dock
-775=Returning to dock - Lawn complete
-776=Returning to dock - Relocalising
+```
+Number Indego_State { channel="boschindego:indego:lawnmower:state" }
+Number Indego_ErrorCode { channel="boschindego:indego:lawnmower:errorcode" }
+Number Indego_StateCode { channel="boschindego:indego:lawnmower:statecode" }
+String Indego_TextualState { channel="boschindego:indego:lawnmower:textualstate" }
+Number Indego_Ready { channel="boschindego:indego:lawnmower:ready" }
+Dimmer Indego_Mowed { channel="boschindego:indego:lawnmower:mowed" }
+```
+
+### `indego.sitemap` File
+
+```
+Switch item=Indego_State mappings=[1="Mow", 2="Return",3="Pause"]
```
diff --git a/bundles/org.openhab.binding.boschindego/pom.xml b/bundles/org.openhab.binding.boschindego/pom.xml
index e570380da..c75c09305 100644
--- a/bundles/org.openhab.binding.boschindego/pom.xml
+++ b/bundles/org.openhab.binding.boschindego/pom.xml
@@ -14,35 +14,4 @@
openHAB Add-ons :: Bundles :: Bosch Indego Binding
-
- httpclient-osgi,httpcore-osgi,commons-codec
-
-
-
-
- de.zazaz.iot.bosch.indego
- bosch-indego-controller-lib
- 0.8
- compile
-
-
- org.apache.httpcomponents
- httpclient-osgi
- 4.5.5
- compile
-
-
- org.apache.httpcomponents
- httpcore-osgi
- 4.4.9
- compile
-
-
- commons-codec
- commons-codec
- 1.10
- compile
-
-
-
diff --git a/bundles/org.openhab.binding.boschindego/src/main/feature/feature.xml b/bundles/org.openhab.binding.boschindego/src/main/feature/feature.xml
index 5dfb52614..13655c46c 100644
--- a/bundles/org.openhab.binding.boschindego/src/main/feature/feature.xml
+++ b/bundles/org.openhab.binding.boschindego/src/main/feature/feature.xml
@@ -4,10 +4,6 @@
openhab-runtime-base
- openhab.tp-jackson
- mvn:org.apache.httpcomponents/httpcore-osgi/4.4.9
- mvn:org.apache.httpcomponents/httpclient-osgi/4.5.5
- mvn:commons-codec/commons-codec/1.10mvn:org.openhab.addons.bundles/org.openhab.binding.boschindego/${project.version}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java
index 36dcb580c..546915725 100644
--- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java
@@ -12,6 +12,8 @@
*/
package org.openhab.binding.boschindego.internal;
+import java.util.Set;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
@@ -36,4 +38,6 @@ public class BoschIndegoBindingConstants {
public static final String ERRORCODE = "errorcode";
public static final String STATECODE = "statecode";
public static final String READY = "ready";
+
+ public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_INDEGO);
}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoHandlerFactory.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoHandlerFactory.java
index 2bd7e1ae4..de97ac1fd 100644
--- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoHandlerFactory.java
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoHandlerFactory.java
@@ -14,16 +14,20 @@ package org.openhab.binding.boschindego.internal;
import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.THING_TYPE_INDEGO;
-import java.util.Collections;
-import java.util.Set;
-
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.boschindego.internal.handler.BoschIndegoHandler;
+import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
/**
* The {@link BoschIndegoHandlerFactory} is responsible for creating things and thing
@@ -31,22 +35,30 @@ import org.osgi.service.component.annotations.Component;
*
* @author Jonas Fleck - Initial contribution
*/
+@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.boschindego")
public class BoschIndegoHandlerFactory extends BaseThingHandlerFactory {
- private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_INDEGO);
+ private final HttpClient httpClient;
- @Override
- public boolean supportsThingType(ThingTypeUID thingTypeUID) {
- return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ @Activate
+ public BoschIndegoHandlerFactory(@Reference HttpClientFactory httpClientFactory,
+ ComponentContext componentContext) {
+ super.activate(componentContext);
+ this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
- protected ThingHandler createHandler(Thing thing) {
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return BoschIndegoBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
- if (thingTypeUID.equals(THING_TYPE_INDEGO)) {
- return new BoschIndegoHandler(thing);
+ if (THING_TYPE_INDEGO.equals(thingTypeUID)) {
+ return new BoschIndegoHandler(thing, httpClient);
}
return null;
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/DeviceStatus.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/DeviceStatus.java
new file mode 100644
index 000000000..b4a85e58d
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/DeviceStatus.java
@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal;
+
+import static java.util.Map.entry;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
+
+/**
+ * {@link DeviceStatus} describes status codes from the device with corresponding
+ * ready state and associated command.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceStatus {
+
+ private static final Map STATUS_MAP = Map.ofEntries(
+ entry(0, new DeviceStatus("Reading status", false, DeviceCommand.RETURN)),
+ entry(257, new DeviceStatus("Charging", false, DeviceCommand.RETURN)),
+ entry(258, new DeviceStatus("Docked", true, DeviceCommand.RETURN)),
+ entry(259, new DeviceStatus("Docked - Software update", false, DeviceCommand.RETURN)),
+ entry(260, new DeviceStatus("Docked", true, DeviceCommand.RETURN)),
+ entry(261, new DeviceStatus("Docked", true, DeviceCommand.RETURN)),
+ entry(262, new DeviceStatus("Docked - Loading map", false, DeviceCommand.MOW)),
+ entry(263, new DeviceStatus("Docked - Saving map", false, DeviceCommand.RETURN)),
+ entry(513, new DeviceStatus("Mowing", false, DeviceCommand.MOW)),
+ entry(514, new DeviceStatus("Relocalising", false, DeviceCommand.MOW)),
+ entry(515, new DeviceStatus("Loading map", false, DeviceCommand.MOW)),
+ entry(516, new DeviceStatus("Learning lawn", false, DeviceCommand.MOW)),
+ entry(517, new DeviceStatus("Paused", true, DeviceCommand.PAUSE)),
+ entry(518, new DeviceStatus("Border cut", false, DeviceCommand.MOW)),
+ entry(519, new DeviceStatus("Idle in lawn", true, DeviceCommand.MOW)),
+ entry(769, new DeviceStatus("Returning to dock", false, DeviceCommand.RETURN)),
+ entry(770, new DeviceStatus("Returning to dock", false, DeviceCommand.RETURN)),
+ entry(771, new DeviceStatus("Returning to dock - Battery low", false, DeviceCommand.RETURN)),
+ entry(772, new DeviceStatus("Returning to dock - Calendar timeslot ended", false, DeviceCommand.RETURN)),
+ entry(773, new DeviceStatus("Returning to dock - Battery temp range", false, DeviceCommand.RETURN)),
+ entry(774, new DeviceStatus("Returning to dock", false, DeviceCommand.RETURN)),
+ entry(775, new DeviceStatus("Returning to dock - Lawn complete", false, DeviceCommand.RETURN)),
+ entry(776, new DeviceStatus("Returning to dock - Relocalising", false, DeviceCommand.RETURN)),
+ entry(1025, new DeviceStatus("Diagnostic mode", false, null)),
+ entry(1026, new DeviceStatus("End of life", false, null)),
+ entry(1281, new DeviceStatus("Software update", false, null)),
+ entry(64513, new DeviceStatus("Docked", true, DeviceCommand.RETURN)));
+
+ private String message;
+
+ private boolean isReadyToMow;
+
+ private @Nullable DeviceCommand associatedCommand;
+
+ private DeviceStatus(String message, boolean isReadyToMow, @Nullable DeviceCommand associatedCommand) {
+ this.message = message;
+ this.isReadyToMow = isReadyToMow;
+ this.associatedCommand = associatedCommand;
+ }
+
+ /**
+ * Returns a {@link DeviceStatus} instance describing the status code.
+ *
+ * @param code the status code
+ * @return the {@link DeviceStatus} providing additional context for the code
+ */
+ public static DeviceStatus fromCode(int code) {
+ DeviceStatus status = STATUS_MAP.get(code);
+ if (status != null) {
+ return status;
+ }
+
+ DeviceCommand command = null;
+ switch (code & 0xff00) {
+ case 0x100:
+ command = DeviceCommand.RETURN;
+ break;
+ case 0x200:
+ command = DeviceCommand.MOW;
+ break;
+ case 0x300:
+ command = DeviceCommand.RETURN;
+ break;
+ }
+
+ return new DeviceStatus(String.format("Unknown status code %d", code), false, command);
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public boolean isReadyToMow() {
+ return isReadyToMow;
+ }
+
+ public @Nullable DeviceCommand getAssociatedCommand() {
+ return associatedCommand;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java
new file mode 100644
index 000000000..eb7d92901
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java
@@ -0,0 +1,514 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal;
+
+import java.net.URI;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpResponseException;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
+import org.openhab.binding.boschindego.internal.dto.PredictiveAdjustment;
+import org.openhab.binding.boschindego.internal.dto.PredictiveStatus;
+import org.openhab.binding.boschindego.internal.dto.request.AuthenticationRequest;
+import org.openhab.binding.boschindego.internal.dto.request.SetStateRequest;
+import org.openhab.binding.boschindego.internal.dto.response.AuthenticationResponse;
+import org.openhab.binding.boschindego.internal.dto.response.DeviceCalendarResponse;
+import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse;
+import org.openhab.binding.boschindego.internal.dto.response.LocationWeatherResponse;
+import org.openhab.binding.boschindego.internal.dto.response.PredictiveCuttingTimeResponse;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonParseException;
+
+/**
+ * Controller for communicating with a Bosch Indego device through Bosch services.
+ * This class provides methods for retrieving state information as well as controlling
+ * the device.
+ *
+ * The implementation is based on zazaz-de/iot-device-bosch-indego-controller, but
+ * rewritten from scratch to use Jetty HTTP client for HTTP communication and GSON for
+ * JSON parsing. Thanks to Oliver Schünemann for providing the original implementation.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class IndegoController {
+
+ private static final String BASE_URL = "https://api.indego.iot.bosch-si.com/api/v1/";
+ private static final URI BASE_URI = URI.create(BASE_URL);
+ private static final String SERIAL_NUMBER_SUBPATH = "alms/";
+ private static final String SSO_COOKIE_NAME = "BOSCH_INDEGO_SSO";
+ private static final String CONTEXT_HEADER_NAME = "x-im-context-id";
+ private static final String CONTENT_TYPE_HEADER = "application/json";
+
+ private final Logger logger = LoggerFactory.getLogger(IndegoController.class);
+ private final String basicAuthenticationHeader;
+ private final Gson gson = new Gson();
+ private final HttpClient httpClient;
+
+ private IndegoSession session = new IndegoSession();
+
+ /**
+ * Initialize the controller instance.
+ *
+ * @param username the username for authenticating
+ * @param password the password
+ */
+ public IndegoController(HttpClient httpClient, String username, String password) {
+ this.httpClient = httpClient;
+ basicAuthenticationHeader = "Basic "
+ + Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
+ }
+
+ /**
+ * Authenticate with server and store session context and serial number.
+ *
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ private void authenticate() throws IndegoAuthenticationException, IndegoException {
+ try {
+ Request request = httpClient.newRequest(BASE_URL + "authenticate").method(HttpMethod.POST)
+ .header(HttpHeader.AUTHORIZATION, basicAuthenticationHeader);
+
+ AuthenticationRequest authRequest = new AuthenticationRequest();
+ authRequest.device = "";
+ authRequest.osType = "Android";
+ authRequest.osVersion = "4.0";
+ authRequest.deviceManufacturer = "unknown";
+ authRequest.deviceType = "unknown";
+ String json = gson.toJson(authRequest);
+ request.content(new StringContentProvider(json));
+ request.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_HEADER);
+
+ if (logger.isTraceEnabled()) {
+ logger.trace("POST request for {}", BASE_URL + "authenticate");
+ }
+
+ ContentResponse response = sendRequest(request);
+ int status = response.getStatus();
+ if (status == HttpStatus.UNAUTHORIZED_401) {
+ throw new IndegoAuthenticationException("Authentication was rejected");
+ }
+ if (!HttpStatus.isSuccess(status)) {
+ throw new IndegoAuthenticationException("The request failed with HTTP error: " + status);
+ }
+
+ String jsonResponse = response.getContentAsString();
+ if (jsonResponse.isEmpty()) {
+ throw new IndegoInvalidResponseException("No content returned");
+ }
+ logger.trace("JSON response: '{}'", jsonResponse);
+
+ AuthenticationResponse authenticationResponse = gson.fromJson(jsonResponse, AuthenticationResponse.class);
+ if (authenticationResponse == null) {
+ throw new IndegoInvalidResponseException("Response could not be parsed as AuthenticationResponse");
+ }
+ session = new IndegoSession(authenticationResponse.contextId, authenticationResponse.serialNumber,
+ getContextExpirationTimeFromCookie());
+ logger.debug("Initialized session {}", session);
+ } catch (JsonParseException e) {
+ throw new IndegoInvalidResponseException("Error parsing AuthenticationResponse", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IndegoException(e);
+ } catch (TimeoutException | ExecutionException e) {
+ throw new IndegoException(e);
+ }
+ }
+
+ /**
+ * Get context expiration time as a calculated {@link Instant} relative to now.
+ * The information is obtained from max age in the Bosch Indego SSO cookie.
+ * Please note that this cookie is only sent initially when authenticating, so
+ * the value will not be subject to any updates.
+ *
+ * @return expiration time as {@link Instant} or {@link Instant#MIN} if not present
+ */
+ private Instant getContextExpirationTimeFromCookie() {
+ return httpClient.getCookieStore().get(BASE_URI).stream().filter(c -> SSO_COOKIE_NAME.equals(c.getName()))
+ .findFirst().map(c -> {
+ return Instant.now().plusSeconds(c.getMaxAge());
+ }).orElseGet(() -> {
+ return Instant.MIN;
+ });
+ }
+
+ /**
+ * Wraps {@link #getRequest(String, Class)} into an authenticated session.
+ *
+ * @param path the relative path to which the request should be sent
+ * @param dtoClass the DTO class to which the JSON result should be deserialized
+ * @return the deserialized DTO from the JSON response
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ private T getRequestWithAuthentication(String path, Class extends T> dtoClass)
+ throws IndegoAuthenticationException, IndegoException {
+ if (!session.isValid()) {
+ authenticate();
+ }
+ try {
+ logger.debug("Session {} valid, skipping authentication", session);
+ return getRequest(path, dtoClass);
+ } catch (IndegoAuthenticationException e) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Context rejected", e);
+ } else {
+ logger.debug("Context rejected: {}", e.getMessage());
+ }
+ session.invalidate();
+ authenticate();
+ return getRequest(path, dtoClass);
+ }
+ }
+
+ /**
+ * Sends a GET request to the server and returns the deserialized JSON response.
+ *
+ * @param path the relative path to which the request should be sent
+ * @param dtoClass the DTO class to which the JSON result should be deserialized
+ * @return the deserialized DTO from the JSON response
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ private T getRequest(String path, Class extends T> dtoClass)
+ throws IndegoAuthenticationException, IndegoException {
+ try {
+ Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET).header(CONTEXT_HEADER_NAME,
+ session.getContextId());
+ if (logger.isTraceEnabled()) {
+ logger.trace("GET request for {}", BASE_URL + path);
+ }
+ ContentResponse response = sendRequest(request);
+ int status = response.getStatus();
+ if (status == HttpStatus.UNAUTHORIZED_401) {
+ // This will currently not happen because "WWW-Authenticate" header is missing; see below.
+ throw new IndegoAuthenticationException("Context rejected");
+ }
+ if (!HttpStatus.isSuccess(status)) {
+ throw new IndegoAuthenticationException("The request failed with HTTP error: " + status);
+ }
+ String jsonResponse = response.getContentAsString();
+ if (jsonResponse.isEmpty()) {
+ throw new IndegoInvalidResponseException("No content returned");
+ }
+ logger.trace("JSON response: '{}'", jsonResponse);
+
+ @Nullable
+ T result = gson.fromJson(jsonResponse, dtoClass);
+ if (result == null) {
+ throw new IndegoInvalidResponseException("Parsed response is null");
+ }
+ return result;
+ } catch (JsonParseException e) {
+ throw new IndegoInvalidResponseException("Error parsing response", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IndegoException(e);
+ } catch (TimeoutException e) {
+ throw new IndegoException(e);
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause != null && cause instanceof HttpResponseException) {
+ Response response = ((HttpResponseException) cause).getResponse();
+ if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
+ /*
+ * When contextId is not valid, the service will respond with HTTP code 401 without
+ * any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
+ * HttpResponseException. We need to handle this in order to attempt
+ * reauthentication.
+ */
+ throw new IndegoAuthenticationException("Context rejected", e);
+ }
+ }
+ throw new IndegoException(e);
+ }
+ }
+
+ /**
+ * Wraps {@link #putRequest(String, Object)} into an authenticated session.
+ *
+ * @param path the relative path to which the request should be sent
+ * @param requestDto the DTO which should be sent to the server as JSON
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ private void putRequestWithAuthentication(String path, Object requestDto)
+ throws IndegoAuthenticationException, IndegoException {
+ if (!session.isValid()) {
+ authenticate();
+ }
+ try {
+ logger.debug("Session {} valid, skipping authentication", session);
+ putRequest(path, requestDto);
+ } catch (IndegoAuthenticationException e) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Context rejected", e);
+ } else {
+ logger.debug("Context rejected: {}", e.getMessage());
+ }
+ session.invalidate();
+ authenticate();
+ putRequest(path, requestDto);
+ }
+ }
+
+ /**
+ * Sends a PUT request to the server.
+ *
+ * @param path the relative path to which the request should be sent
+ * @param requestDto the DTO which should be sent to the server as JSON
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ private void putRequest(String path, Object requestDto) throws IndegoAuthenticationException, IndegoException {
+ try {
+ Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.PUT)
+ .header(CONTEXT_HEADER_NAME, session.getContextId())
+ .header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_HEADER);
+ String payload = gson.toJson(requestDto);
+ request.content(new StringContentProvider(payload));
+ if (logger.isTraceEnabled()) {
+ logger.trace("PUT request for {} with payload '{}'", BASE_URL + path, payload);
+ }
+ ContentResponse response = sendRequest(request);
+ int status = response.getStatus();
+ if (status == HttpStatus.UNAUTHORIZED_401) {
+ // This will currently not happen because "WWW-Authenticate" header is missing; see below.
+ throw new IndegoAuthenticationException("Context rejected");
+ }
+ if (status == HttpStatus.INTERNAL_SERVER_ERROR_500) {
+ throw new IndegoInvalidCommandException("The request failed with HTTP error: " + status);
+ }
+ if (!HttpStatus.isSuccess(status)) {
+ throw new IndegoException("The request failed with error: " + status);
+ }
+ } catch (JsonParseException e) {
+ throw new IndegoInvalidResponseException("Error serializing request", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IndegoException(e);
+ } catch (TimeoutException e) {
+ throw new IndegoException(e);
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause != null && cause instanceof HttpResponseException) {
+ Response response = ((HttpResponseException) cause).getResponse();
+ if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
+ /*
+ * When contextId is not valid, the service will respond with HTTP code 401 without
+ * any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
+ * HttpResponseException. We need to handle this in order to attempt
+ * reauthentication.
+ */
+ throw new IndegoAuthenticationException("Context rejected", e);
+ }
+ }
+ throw new IndegoException(e);
+ }
+ }
+
+ /**
+ * Send request. This method exists for the purpose of avoiding multiple calls to
+ * the server at the same time.
+ *
+ * @param request the {@link Request} to send
+ * @return a {@link ContentResponse} for this request
+ * @throws InterruptedException if send thread is interrupted
+ * @throws TimeoutException if send times out
+ * @throws ExecutionException if execution fails
+ */
+ private synchronized ContentResponse sendRequest(Request request)
+ throws InterruptedException, TimeoutException, ExecutionException {
+ return request.send();
+ }
+
+ /**
+ * Gets serial number of the associated Indego device
+ *
+ * @return the serial number of the device
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public String getSerialNumber() throws IndegoAuthenticationException, IndegoException {
+ if (!session.isInitialized()) {
+ logger.debug("Session not yet initialized when serial number was requested; authenticating...");
+ authenticate();
+ }
+ return session.getSerialNumber();
+ }
+
+ /**
+ * Queries the device state from the server.
+ *
+ * @return the device state
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public DeviceStateResponse getState() throws IndegoAuthenticationException, IndegoException {
+ return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/state",
+ DeviceStateResponse.class);
+ }
+
+ /**
+ * Queries the calendar.
+ *
+ * @return the calendar
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public DeviceCalendarResponse getCalendar() throws IndegoAuthenticationException, IndegoException {
+ DeviceCalendarResponse calendar = getRequestWithAuthentication(
+ SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/calendar", DeviceCalendarResponse.class);
+ return calendar;
+ }
+
+ /**
+ * Sends a command to the Indego device.
+ *
+ * @param command the control command to send to the device
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoInvalidCommandException if the command was not processed correctly
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public void sendCommand(DeviceCommand command)
+ throws IndegoAuthenticationException, IndegoInvalidCommandException, IndegoException {
+ SetStateRequest request = new SetStateRequest();
+ request.state = command.getActionCode();
+ putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/state", request);
+ }
+
+ /**
+ * Queries the predictive weather forecast.
+ *
+ * @return the weather forecast DTO
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public LocationWeatherResponse getWeather() throws IndegoAuthenticationException, IndegoException {
+ return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/weather",
+ LocationWeatherResponse.class);
+ }
+
+ /**
+ * Queries the predictive adjustment.
+ *
+ * @return the predictive adjustment
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public int getPredictiveAdjustment() throws IndegoAuthenticationException, IndegoException {
+ return getRequestWithAuthentication(
+ SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/useradjustment",
+ PredictiveAdjustment.class).adjustment;
+ }
+
+ /**
+ * Sets the predictive adjustment.
+ *
+ * @param adjust the predictive adjustment
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public void setPredictiveAdjustment(final int adjust) throws IndegoAuthenticationException, IndegoException {
+ final PredictiveAdjustment adjustment = new PredictiveAdjustment();
+ adjustment.adjustment = adjust;
+ putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/useradjustment",
+ adjustment);
+ }
+
+ /**
+ * Queries predictive moving.
+ *
+ * @return predictive moving
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public boolean getPredictiveMoving() throws IndegoAuthenticationException, IndegoException {
+ final PredictiveStatus status = getRequestWithAuthentication(
+ SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive", PredictiveStatus.class);
+ return status.enabled;
+ }
+
+ /**
+ * Sets predictive moving.
+ *
+ * @param enable
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public void setPredictiveMoving(final boolean enable) throws IndegoAuthenticationException, IndegoException {
+ final PredictiveStatus status = new PredictiveStatus();
+ status.enabled = enable;
+ putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive", status);
+ }
+
+ /**
+ * Queries predictive next cutting as {@link Instant}.
+ *
+ * @return predictive next cutting
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public Instant getPredictiveNextCutting() throws IndegoAuthenticationException, IndegoException {
+ final PredictiveCuttingTimeResponse nextCutting = getRequestWithAuthentication(
+ SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/nextcutting",
+ PredictiveCuttingTimeResponse.class);
+ return nextCutting.getNextCutting();
+ }
+
+ /**
+ * Queries predictive exclusion time.
+ *
+ * @return predictive exclusion time DTO
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public DeviceCalendarResponse getPredictiveExclusionTime() throws IndegoAuthenticationException, IndegoException {
+ final DeviceCalendarResponse calendar = getRequestWithAuthentication(
+ SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/calendar", DeviceCalendarResponse.class);
+ return calendar;
+ }
+
+ /**
+ * Sets predictive exclusion time.
+ *
+ * @param calendar calendar DTO
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public void setPredictiveExclusionTime(final DeviceCalendarResponse calendar)
+ throws IndegoAuthenticationException, IndegoException {
+ putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/calendar", calendar);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoSession.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoSession.java
new file mode 100644
index 000000000..cc75149ac
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoSession.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Session for storing Bosch Indego context information.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class IndegoSession {
+
+ private static final Duration DEFAULT_EXPIRATION_PERIOD = Duration.ofSeconds(10);
+
+ private String contextId;
+ private String serialNumber;
+ private Instant expirationTime;
+
+ public IndegoSession() {
+ this("", "", Instant.MIN);
+ }
+
+ public IndegoSession(String contextId, String serialNumber, Instant expirationTime) {
+ this.contextId = contextId;
+ this.serialNumber = serialNumber;
+ this.expirationTime = expirationTime.equals(Instant.MIN) ? Instant.now().plus(DEFAULT_EXPIRATION_PERIOD)
+ : expirationTime;
+ }
+
+ /**
+ * Get context id for HTTP requests (headers "x-im-context-id: " and
+ * "Cookie: BOSCH_INDEGO_SSO=").
+ *
+ * @return current context id
+ */
+ public String getContextId() {
+ return contextId;
+ }
+
+ /**
+ * Get serial number of device.
+ *
+ * @return serial number
+ */
+ public String getSerialNumber() {
+ return serialNumber;
+ }
+
+ /**
+ * Get expiration time of session as {@link Instant}.
+ *
+ * @return expiration time
+ */
+ public Instant getExpirationTime() {
+ return expirationTime;
+ }
+
+ /**
+ * Check if session is initialized, i.e. has serial number.
+ *
+ * @see #isValid()
+ * @return true if session is initialized
+ */
+ public boolean isInitialized() {
+ return !serialNumber.isEmpty();
+ }
+
+ /**
+ * Check if session is valid, i.e. has not yet expired.
+ *
+ * @return true if session is still valid
+ */
+ public boolean isValid() {
+ return !contextId.isEmpty() && expirationTime.isAfter(Instant.now());
+ }
+
+ /**
+ * Invalidate session.
+ */
+ public void invalidate() {
+ contextId = "";
+ expirationTime = Instant.MIN;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s (serialNumber %s, expirationTime %s)", contextId, serialNumber, expirationTime);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoStateConstants.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/config/BoschIndegoConfiguration.java
similarity index 54%
rename from bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoStateConstants.java
rename to bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/config/BoschIndegoConfiguration.java
index 51fc145a3..9512d5d4b 100644
--- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoStateConstants.java
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/config/BoschIndegoConfiguration.java
@@ -10,20 +10,19 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.boschindego.internal;
+package org.openhab.binding.boschindego.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
/**
+ * Configuration for the Bosch Indego thing.
*
- * @author Jonas Fleck - Initial contribution
+ * @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
-public class IndegoStateConstants {
-
- public static final int STATE_DOCKED_1 = 258;
- public static final int STATE_DOCKED_2 = 260;
- public static final int STATE_DOCKED_3 = 261;
- public static final int STATE_PAUSED = 517;
- public static final int STATE_IDLE_IN_LAWN = 519;
+public class BoschIndegoConfiguration {
+ public @Nullable String username;
+ public @Nullable String password;
+ public long refresh = 180;
}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/DeviceCommand.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/DeviceCommand.java
new file mode 100644
index 000000000..6ed530a93
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/DeviceCommand.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschindego.internal.dto.request.SetStateRequest;
+
+/**
+ * Commands supported by the device.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public enum DeviceCommand {
+
+ MOW(SetStateRequest.STATE_MOW),
+ PAUSE(SetStateRequest.STATE_PAUSE),
+ RETURN(SetStateRequest.STATE_RETURN);
+
+ private String actionCode;
+
+ DeviceCommand(String actionCode) {
+ this.actionCode = actionCode;
+ }
+
+ public String getActionCode() {
+ return actionCode;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/PredictiveAdjustment.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/PredictiveAdjustment.java
new file mode 100644
index 000000000..753b21c43
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/PredictiveAdjustment.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Request/response for user adjustment.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class PredictiveAdjustment {
+ @SerializedName("user_adjustment")
+ public int adjustment;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/PredictiveStatus.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/PredictiveStatus.java
new file mode 100644
index 000000000..839e61a0c
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/PredictiveStatus.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto;
+
+/**
+ * Request/response for predictive status.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class PredictiveStatus {
+ public boolean enabled;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/request/AuthenticationRequest.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/request/AuthenticationRequest.java
new file mode 100644
index 000000000..26da9294f
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/request/AuthenticationRequest.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.request;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Request for authenticating with server
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class AuthenticationRequest {
+
+ @SerializedName("accept_tc_id")
+ public String acceptTcId;
+
+ public String device;
+
+ @SerializedName("os_type")
+ public String osType;
+
+ @SerializedName("os_version")
+ public String osVersion;
+
+ @SerializedName("dvc_manuf")
+ public String deviceManufacturer;
+
+ @SerializedName("dvc_type")
+ public String deviceType;
+
+ public AuthenticationRequest() {
+ acceptTcId = "202012";
+ }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/request/SetStateRequest.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/request/SetStateRequest.java
new file mode 100644
index 000000000..598015715
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/request/SetStateRequest.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.request;
+
+/**
+ * Request for setting a new device state
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class SetStateRequest {
+
+ public static final String STATE_MOW = "mow";
+
+ public static final String STATE_PAUSE = "pause";
+
+ public static final String STATE_RETURN = "returnToDock";
+
+ public String state;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/AuthenticationResponse.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/AuthenticationResponse.java
new file mode 100644
index 000000000..09a829217
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/AuthenticationResponse.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Response from authenticating with server.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class AuthenticationResponse {
+
+ public String contextId;
+
+ public String userId;
+
+ @SerializedName("alm_sn")
+ public String serialNumber;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/DeviceCalendarResponse.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/DeviceCalendarResponse.java
new file mode 100644
index 000000000..7d5b0264c
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/DeviceCalendarResponse.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response;
+
+import org.openhab.binding.boschindego.internal.dto.response.calendar.DeviceCalendarEntry;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Response for device calendar.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class DeviceCalendarResponse {
+
+ @SerializedName("sel_cal")
+ public int selectedEntryNumber;
+
+ @SerializedName("cals")
+ public DeviceCalendarEntry[] entries;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/DeviceStateResponse.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/DeviceStateResponse.java
new file mode 100644
index 000000000..b8e4a9f27
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/DeviceStateResponse.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response;
+
+import org.openhab.binding.boschindego.internal.dto.response.runtime.DeviceStateRuntimes;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Response after querying the device status.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class DeviceStateResponse {
+
+ public int state;
+
+ public int error;
+
+ public boolean enabled;
+
+ @SerializedName("map_update_available")
+ public boolean mapUpdateAvailable;
+
+ public int mowed;
+
+ @SerializedName("mowmode")
+ public long mowMode;
+
+ public int xPos;
+
+ public int yPos;
+
+ public DeviceStateRuntimes runtime;
+
+ @SerializedName("mowed_ts")
+ public long mowedTimestamp;
+
+ @SerializedName("mapsvgcache_ts")
+ public long mapSvgCacheTimestamp;
+
+ @SerializedName("svg_xPos")
+ public int svgXPos;
+
+ @SerializedName("svg_yPos")
+ public int svgYPos;
+
+ @SerializedName("config_change")
+ public boolean configChange;
+
+ @SerializedName("mow_trig")
+ public boolean mowTrigger;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/LocationWeatherResponse.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/LocationWeatherResponse.java
new file mode 100644
index 000000000..5ec094d5d
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/LocationWeatherResponse.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response;
+
+import org.openhab.binding.boschindego.internal.dto.response.weather.Weather;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Response for weather forecast.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class LocationWeatherResponse {
+
+ @SerializedName("LocationWeather")
+ public Weather weather;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveCuttingTimeResponse.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveCuttingTimeResponse.java
new file mode 100644
index 000000000..27d78e458
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveCuttingTimeResponse.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response;
+
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Response for next cutting time.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class PredictiveCuttingTimeResponse {
+ @SerializedName("mow_next")
+ public String nextCutting;
+
+ public Instant getNextCutting() {
+ try {
+ return ZonedDateTime.parse(nextCutting).toInstant();
+ } catch (final DateTimeParseException e) {
+ // Ignored
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/calendar/DeviceCalendarDayEntry.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/calendar/DeviceCalendarDayEntry.java
new file mode 100644
index 000000000..bfdf1da20
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/calendar/DeviceCalendarDayEntry.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response.calendar;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Device calendar day entry.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class DeviceCalendarDayEntry {
+
+ @SerializedName("day")
+ public int number;
+
+ @SerializedName("slots")
+ public DeviceCalendarDaySlot[] slots;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/calendar/DeviceCalendarDaySlot.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/calendar/DeviceCalendarDaySlot.java
new file mode 100644
index 000000000..9c5eb2fb2
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/calendar/DeviceCalendarDaySlot.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response.calendar;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Device calendar day slot.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class DeviceCalendarDaySlot {
+
+ @SerializedName("En")
+ public boolean enabled;
+
+ @SerializedName("StHr")
+ public int startHour;
+
+ @SerializedName("StMin")
+ public int startMinute;
+
+ @SerializedName("EnHr")
+ public int endHour;
+
+ @SerializedName("EnMin")
+ public int endMinute;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/calendar/DeviceCalendarEntry.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/calendar/DeviceCalendarEntry.java
new file mode 100644
index 000000000..b4c6509d2
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/calendar/DeviceCalendarEntry.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response.calendar;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Device calendar entry.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class DeviceCalendarEntry {
+
+ @SerializedName("cal")
+ public int number;
+
+ public DeviceCalendarDayEntry[] days;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/runtime/DeviceStateRuntime.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/runtime/DeviceStateRuntime.java
new file mode 100644
index 000000000..0061ffbfa
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/runtime/DeviceStateRuntime.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.boschindego.internal.dto.response.runtime;
+
+/**
+ * Detailed runtime information for {@link DeviceStateRuntimes}
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class DeviceStateRuntime {
+ public long operate;
+
+ public long charge;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/runtime/DeviceStateRuntimes.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/runtime/DeviceStateRuntimes.java
new file mode 100644
index 000000000..dca9cb667
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/runtime/DeviceStateRuntimes.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.boschindego.internal.dto.response.runtime;
+
+/**
+ * Total/session runtime information for {@link DeviceStateResponse}
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class DeviceStateRuntimes {
+ public DeviceStateRuntime total;
+
+ public DeviceStateRuntime session;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Forecast.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Forecast.java
new file mode 100644
index 000000000..4232f2f73
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Forecast.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response.weather;
+
+/**
+ * Forecast.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class Forecast {
+
+ public Interval[] intervals;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Interval.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Interval.java
new file mode 100644
index 000000000..0cd3e382a
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Interval.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response.weather;
+
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Interval.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class Interval {
+
+ @SerializedName("dateTime")
+ public String date;
+
+ public int intervalLength;
+
+ @SerializedName("prrr")
+ public int rain;
+
+ @SerializedName("tt")
+ public float temperature;
+
+ public void setDate(final Instant date) {
+ this.date = date.toString();
+ }
+
+ public Instant getDate() {
+ try {
+ return ZonedDateTime.parse(date).toInstant();
+ } catch (final DateTimeParseException e) {
+ // Ignored
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Location.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Location.java
new file mode 100644
index 000000000..d7fa1dbb8
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Location.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response.weather;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Location.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class Location {
+
+ @SerializedName("name")
+ public String town;
+
+ public String country;
+
+ @SerializedName("tzn")
+ public String timeZone;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Weather.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Weather.java
new file mode 100644
index 000000000..8b7322623
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/weather/Weather.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response.weather;
+
+/**
+ * Weather.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class Weather {
+
+ public Location location;
+ public Forecast forecast;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoAuthenticationException.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoAuthenticationException.java
new file mode 100644
index 000000000..258e765b7
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoAuthenticationException.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.exceptions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link IndegoAuthenticationException} is thrown on authentication failure, for example
+ * when username or password is wrong.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class IndegoAuthenticationException extends IndegoException {
+
+ private static final long serialVersionUID = -9047922366108411751L;
+
+ public IndegoAuthenticationException(String message) {
+ super(message);
+ }
+
+ public IndegoAuthenticationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoException.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoException.java
new file mode 100644
index 000000000..20473b130
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoException.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.exceptions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link IndegoException} is a generic Indego exception thrown in case
+ * of communication failure or unexpected response. It is intended to
+ * be derived by specialized exceptions.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class IndegoException extends Exception {
+
+ private static final long serialVersionUID = 6673869982385647268L;
+
+ public IndegoException(String message) {
+ super(message);
+ }
+
+ public IndegoException(Throwable cause) {
+ super(cause);
+ }
+
+ public IndegoException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoInvalidCommandException.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoInvalidCommandException.java
new file mode 100644
index 000000000..024a9efb1
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoInvalidCommandException.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.exceptions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link IndegoInvalidCommandException} is thrown when a command is rejected by the device.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class IndegoInvalidCommandException extends IndegoException {
+
+ private static final long serialVersionUID = -2946398731437793113L;
+
+ public IndegoInvalidCommandException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoInvalidResponseException.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoInvalidResponseException.java
new file mode 100644
index 000000000..d9b4495ed
--- /dev/null
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoInvalidResponseException.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.exceptions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link IndegoInvalidResponseException} is thrown in case of invalid response from the
+ * Bosch Indego service.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class IndegoInvalidResponseException extends IndegoException {
+
+ private static final long serialVersionUID = -4236849226899489934L;
+
+ public IndegoInvalidResponseException(String message) {
+ super(message);
+ }
+
+ public IndegoInvalidResponseException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java
index fcd77177c..868e1be56 100644
--- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java
+++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java
@@ -13,15 +13,20 @@
package org.openhab.binding.boschindego.internal.handler;
import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
-import static org.openhab.binding.boschindego.internal.IndegoStateConstants.*;
-import java.math.BigDecimal;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.Queue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.boschindego.internal.DeviceStatus;
+import org.openhab.binding.boschindego.internal.IndegoController;
+import org.openhab.binding.boschindego.internal.config.BoschIndegoConfiguration;
+import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
+import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
@@ -36,46 +41,83 @@ import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import de.zazaz.iot.bosch.indego.DeviceCommand;
-import de.zazaz.iot.bosch.indego.DeviceStateInformation;
-import de.zazaz.iot.bosch.indego.DeviceStatus;
-import de.zazaz.iot.bosch.indego.IndegoAuthenticationException;
-import de.zazaz.iot.bosch.indego.IndegoController;
-import de.zazaz.iot.bosch.indego.IndegoException;
-
/**
* The {@link BoschIndegoHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Jonas Fleck - Initial contribution
+ * @author Jacob Laursen - Refactoring, bugfixing and removal of dependency towards abandoned library
*/
+@NonNullByDefault
public class BoschIndegoHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(BoschIndegoHandler.class);
- private final Queue commandQueue = new LinkedList<>();
+ private final HttpClient httpClient;
- private ScheduledFuture> pollFuture;
+ private @NonNullByDefault({}) IndegoController controller;
+ private @Nullable ScheduledFuture> pollFuture;
+ private long refreshRate;
+ private boolean propertiesInitialized;
- // If false the request is already scheduled.
- private boolean shouldReschedule;
-
- public BoschIndegoHandler(Thing thing) {
+ public BoschIndegoHandler(Thing thing, HttpClient httpClient) {
super(thing);
+ this.httpClient = httpClient;
+ }
+
+ @Override
+ public void initialize() {
+ logger.debug("Initializing Indego handler");
+ BoschIndegoConfiguration config = getConfigAs(BoschIndegoConfiguration.class);
+ String username = config.username;
+ String password = config.password;
+
+ if (username == null || username.isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
+ "@text/offline.conf-error.missing-username");
+ return;
+ }
+ if (password == null || password.isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
+ "@text/offline.conf-error.missing-password");
+ return;
+ }
+
+ controller = new IndegoController(httpClient, username, password);
+ refreshRate = config.refresh;
+
+ updateStatus(ThingStatus.UNKNOWN);
+ this.pollFuture = scheduler.scheduleWithFixedDelay(this::refreshState, 0, refreshRate, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void dispose() {
+ logger.debug("Disposing Indego handler");
+ ScheduledFuture> pollFuture = this.pollFuture;
+ if (pollFuture != null) {
+ pollFuture.cancel(true);
+ }
+ this.pollFuture = null;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
- if (command instanceof RefreshType) {
- // Currently manual refreshing is not possible in the moment
+ if (command == RefreshType.REFRESH) {
+ scheduler.submit(() -> this.refreshState());
return;
- } else if (channelUID.getId().equals(STATE) && command instanceof DecimalType) {
- if (command instanceof DecimalType) {
+ }
+ try {
+ if (command instanceof DecimalType && channelUID.getId().equals(STATE)) {
sendCommand(((DecimalType) command).intValue());
}
+ } catch (IndegoAuthenticationException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/offline.comm-error.authentication-failure");
+ } catch (IndegoException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
- private void sendCommand(int commandInt) {
+ private void sendCommand(int commandInt) throws IndegoException {
DeviceCommand command;
switch (commandInt) {
case 1:
@@ -88,94 +130,62 @@ public class BoschIndegoHandler extends BaseThingHandler {
command = DeviceCommand.PAUSE;
break;
default:
- logger.error("Invalid command");
+ logger.warn("Invalid command {}", commandInt);
return;
}
- synchronized (commandQueue) {
- // Add command to queue to avoid blocking
- commandQueue.offer(command);
- if (shouldReschedule) {
- shouldReschedule = false;
- reschedule();
- }
+
+ DeviceStateResponse state = controller.getState();
+ DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
+ if (!verifyCommand(command, deviceStatus, state.error)) {
+ return;
}
+ logger.debug("Sending command {}", command);
+ updateState(TEXTUAL_STATE, UnDefType.UNDEF);
+ controller.sendCommand(command);
+ state = controller.getState();
+ updateStatus(ThingStatus.ONLINE);
+ updateState(state);
}
- private synchronized void poll() {
- // Create controller instance
+ private void refreshState() {
try {
- IndegoController controller = new IndegoController(getConfig().get("username").toString(),
- getConfig().get("password").toString());
- // Connect to server
- controller.connect();
- // Query the device state
- DeviceStateInformation state = controller.getState();
- DeviceStatus statusWithMessage = DeviceStatus.decodeStatusCode(state.getState());
- int status = getStatusFromCommand(statusWithMessage.getAssociatedCommand());
- int mowed = state.getMowed();
- int error = state.getError();
- int statecode = state.getState();
- boolean ready = isReadyToMow(state.getState(), state.getError());
- DeviceCommand commandToSend = null;
- synchronized (commandQueue) {
- // Discard older commands
- while (!commandQueue.isEmpty()) {
- commandToSend = commandQueue.poll();
- }
- // For newer commands a new request is needed
- shouldReschedule = true;
+ if (!propertiesInitialized) {
+ getThing().setProperty(Thing.PROPERTY_SERIAL_NUMBER, controller.getSerialNumber());
+ propertiesInitialized = true;
}
- if (commandToSend != null && verifyCommand(commandToSend, statusWithMessage.getAssociatedCommand(),
- state.getState(), error)) {
- logger.debug("Sending command...");
- updateState(TEXTUAL_STATE, UnDefType.UNDEF);
- controller.sendCommand(commandToSend);
- try {
- for (int i = 0; i < 30 && !Thread.interrupted(); i++) {
- DeviceStateInformation stateTmp = controller.getState();
- if (state.getState() != stateTmp.getState()) {
- state = stateTmp;
- statusWithMessage = DeviceStatus.decodeStatusCode(state.getState());
- status = getStatusFromCommand(statusWithMessage.getAssociatedCommand());
- mowed = state.getMowed();
- error = state.getError();
- statecode = state.getState();
- ready = isReadyToMow(state.getState(), state.getError());
- break;
- }
- Thread.sleep(1000);
- }
- } catch (InterruptedException e) {
- // Nothing to do here
- }
- }
- controller.disconnect();
+ DeviceStateResponse state = controller.getState();
updateStatus(ThingStatus.ONLINE);
- updateState(STATECODE, new DecimalType(statecode));
- updateState(READY, new DecimalType(ready ? 1 : 0));
- updateState(ERRORCODE, new DecimalType(error));
- updateState(MOWED, new PercentType(mowed));
- updateState(STATE, new DecimalType(status));
- updateState(TEXTUAL_STATE, new StringType(statusWithMessage.getMessage()));
-
+ updateState(state);
} catch (IndegoAuthenticationException e) {
- String message = "The login credentials are wrong or another client connected to your Indego account";
- logger.warn(message, e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/offline.comm-error.authentication-failure");
} catch (IndegoException e) {
- logger.warn("An error occurred", e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
- private boolean isReadyToMow(int statusCode, int error) {
- // I don´t know why bosch uses different state codes for the same state.
- return (statusCode == STATE_DOCKED_1 || statusCode == STATE_DOCKED_2 || statusCode == STATE_DOCKED_3
- || statusCode == STATE_PAUSED || statusCode == STATE_IDLE_IN_LAWN) && error == 0;
+ private void updateState(DeviceStateResponse state) {
+ DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
+ int status = getStatusFromCommand(deviceStatus.getAssociatedCommand());
+ int mowed = state.mowed;
+ int error = state.error;
+ int statecode = state.state;
+ boolean ready = isReadyToMow(deviceStatus, state.error);
+
+ updateState(STATECODE, new DecimalType(statecode));
+ updateState(READY, new DecimalType(ready ? 1 : 0));
+ updateState(ERRORCODE, new DecimalType(error));
+ updateState(MOWED, new PercentType(mowed));
+ updateState(STATE, new DecimalType(status));
+ updateState(TEXTUAL_STATE, new StringType(deviceStatus.getMessage()));
}
- private boolean verifyCommand(DeviceCommand command, DeviceCommand state, int statusCode, int errorCode) {
+ private boolean isReadyToMow(DeviceStatus deviceStatus, int error) {
+ return deviceStatus.isReadyToMow() && error == 0;
+ }
+
+ private boolean verifyCommand(DeviceCommand command, DeviceStatus deviceStatus, int errorCode) {
// Mower reported an error
if (errorCode != 0) {
logger.error("The mower reported an error.");
@@ -183,24 +193,27 @@ public class BoschIndegoHandler extends BaseThingHandler {
}
// Command is equal to current state
- if (command == state) {
+ if (command == deviceStatus.getAssociatedCommand()) {
logger.debug("Command is equal to state");
return false;
}
// Cant pause while the mower is docked
- if (command == DeviceCommand.PAUSE && state == DeviceCommand.RETURN) {
- logger.debug("Can´t pause the mower while it´s docked or docking");
+ if (command == DeviceCommand.PAUSE && deviceStatus.getAssociatedCommand() == DeviceCommand.RETURN) {
+ logger.debug("Can't pause the mower while it's docked or docking");
return false;
}
// Command means "MOW" but mower is not ready
- if (command == DeviceCommand.MOW && !isReadyToMow(statusCode, errorCode)) {
- logger.debug("The mower is not ready to mow in the moment");
+ if (command == DeviceCommand.MOW && !isReadyToMow(deviceStatus, errorCode)) {
+ logger.debug("The mower is not ready to mow at the moment");
return false;
}
return true;
}
- private int getStatusFromCommand(DeviceCommand command) {
+ private int getStatusFromCommand(@Nullable DeviceCommand command) {
+ if (command == null) {
+ return 0;
+ }
int status;
switch (command) {
case MOW:
@@ -217,36 +230,4 @@ public class BoschIndegoHandler extends BaseThingHandler {
}
return status;
}
-
- @Override
- public void dispose() {
- super.dispose();
- logger.debug("removing thing..");
- if (pollFuture != null) {
- pollFuture.cancel(true);
- }
- }
-
- private void reschedule() {
- logger.debug("rescheduling");
-
- if (pollFuture != null) {
- pollFuture.cancel(false);
- }
-
- int refreshRate = ((BigDecimal) getConfig().get("refresh")).intValue();
- pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, refreshRate, TimeUnit.SECONDS);
- }
-
- @Override
- public void handleConfigurationUpdate(Map configurationParameters) {
- super.handleConfigurationUpdate(configurationParameters);
- reschedule();
- }
-
- @Override
- public void initialize() {
- updateStatus(ThingStatus.OFFLINE);
- reschedule();
- }
}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties b/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties
index 129903570..d665f9033 100644
--- a/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties
+++ b/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties
@@ -54,5 +54,15 @@ channel-type.boschindego.statecode.state.option.772 = Returning to Dock - Calend
channel-type.boschindego.statecode.state.option.773 = Returning to Dock - Battery temp range
channel-type.boschindego.statecode.state.option.774 = Returning to Dock
channel-type.boschindego.statecode.state.option.775 = Returning to Dock - Lawn complete
-channel-type.boschindego.statecode.state.option.775 = Returning to Dock - Relocalising
+channel-type.boschindego.statecode.state.option.776 = Returning to Dock - Relocalising
+channel-type.boschindego.statecode.state.option.1025 = Diagnostic mode
+channel-type.boschindego.statecode.state.option.1026 = End of life
+channel-type.boschindego.statecode.state.option.1281 = Software update
+channel-type.boschindego.statecode.state.option.64513 = Docked
channel-type.boschindego.textualstate.label = Textual State
+
+# thing status descriptions
+
+offline.comm-error.authentication-failure = The login credentials are wrong or another client is connected to your Indego account
+offline.conf-error.missing-password = Password missing
+offline.conf-error.missing-username = Username missing
diff --git a/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/thing/thing-types.xml
index 6521a1ddb..7faeacbda 100644
--- a/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/thing/thing-types.xml
@@ -48,7 +48,7 @@
Number0 = no error
-
+ Number
@@ -78,7 +78,11 @@
-
+
+
+
+
+