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.
+## Discovery
+
+When the bridge is authorized, the binding can automatically discover Indego mowers connected to the SingleKey ID account.
+
## Thing Configuration
-Currently the binding supports _**indego**_ mowers as a thing type with these configuration parameters:
+### `account` Bridge Configuration
+
+There are no parameters for the bridge.
+However, the bridge is used for managing the [SingleKey ID](https://singlekey-id.com/) digital identity.
+
+#### Authorization
+
+To authorize, please follow these steps:
+
+- In your browser, go to the [Bosch Indego login page](https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect://login&client_id=65bb8c9d-1070-4fb4-aa95-853618acc876&response_type=code&scope=openid%20offline_access%20https://prodindego.onmicrosoft.com/indego-mobile-api/Indego.Mower.User).
+- Select "Bosch ID", enter your e-mail address and password and click "Log-in".
+- In your browser, open Developer Tools.
+- With developer tools showing on the right, go to [Bosch Indego login page](https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect://login&client_id=65bb8c9d-1070-4fb4-aa95-853618acc876&response_type=code&scope=openid%20offline_access%20https://prodindego.onmicrosoft.com/indego-mobile-api/Indego.Mower.User) again.
+- "Please wait..." should now be displayed.
+- Find the `authresp` and copy the code: `com.bosch.indegoconnect://login/?code=<copy this>`
+- Use the openHAB console to authorize with this code: `openhab:boschindego authorize <paste code>`
+
+### `indego` Thing Configuration
-| Parameter | Description | Default |
-|--------------------|-------------------------------------------------------------------|---------|
-| username | Username for the Bosch Indego account | |
-| password | Password for the Bosch Indego account | |
-| refresh | The number of seconds between refreshing device state when idle | 180 |
-| stateActiveRefresh | The number of seconds between refreshing device state when active | 30 |
-| cuttingTimeRefresh | The number of minutes between refreshing last/next cutting time | 60 |
+| Parameter | Description | Default | Required |
+|--------------------|-------------------------------------------------------------------|---------|----------|
+| serialNumber | The serial number of the connected Indego mower | | yes |
+| refresh | The number of seconds between refreshing device state when idle | 180 | no |
+| stateActiveRefresh | The number of seconds between refreshing device state when active | 30 | no |
+| cuttingTimeRefresh | The number of minutes between refreshing last/next cutting time | 60 | no |
## Channels
### `indego.things` File
```java
-boschindego:indego:lawnmower [username="mail@example.com", password="idontneedtocutthelawnagain", refresh=120]
+Bridge boschindego:account:singlekey {
+ Things:
+ Thing indego lawnmower [serialNumber="1234567890", refresh=120]
+}
```
### `indego.items` File
```java
-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" }
-DateTime Indego_LastCutting { channel="boschindego:indego:lawnmower:lastCutting" }
-DateTime Indego_NextCutting { channel="boschindego:indego:lawnmower:nextCutting" }
-Number:ElectricPotential Indego_BatteryVoltage { channel="boschindego:indego:lawnmower:batteryVoltage" }
-Number Indego_BatteryLevel { channel="boschindego:indego:lawnmower:batteryLevel" }
-Switch Indego_LowBattery { channel="boschindego:indego:lawnmower:lowBattery" }
-Number:Temperature Indego_BatteryTemperature { channel="boschindego:indego:lawnmower:batteryTemperature" }
-Number:Area Indego_GardenSize { channel="boschindego:indego:lawnmower:gardenSize" }
-Image Indego_GardenMap { channel="boschindego:indego:lawnmower:gardenMap" }
+Number Indego_State { channel="boschindego:indego:singlekey:lawnmower:state" }
+Number Indego_ErrorCode { channel="boschindego:indego:singlekey:lawnmower:errorcode" }
+Number Indego_StateCode { channel="boschindego:indego:singlekey:lawnmower:statecode" }
+String Indego_TextualState { channel="boschindego:indego:singlekey:lawnmower:textualstate" }
+Number Indego_Ready { channel="boschindego:indego:singlekey:lawnmower:ready" }
+Dimmer Indego_Mowed { channel="boschindego:indego:singlekey:lawnmower:mowed" }
+DateTime Indego_LastCutting { channel="boschindego:indego:singlekey:lawnmower:lastCutting" }
+DateTime Indego_NextCutting { channel="boschindego:indego:singlekey:lawnmower:nextCutting" }
+Number:ElectricPotential Indego_BatteryVoltage { channel="boschindego:indego:singlekey:lawnmower:batteryVoltage" }
+Number Indego_BatteryLevel { channel="boschindego:indego:singlekey:lawnmower:batteryLevel" }
+Switch Indego_LowBattery { channel="boschindego:indego:singlekey:lawnmower:lowBattery" }
+Number:Temperature Indego_BatteryTemperature { channel="boschindego:indego:singlekey:lawnmower:batteryTemperature" }
+Number:Area Indego_GardenSize { channel="boschindego:indego:singlekey:lawnmower:gardenSize" }
+Image Indego_GardenMap { channel="boschindego:indego:singlekey:lawnmower:gardenMap" }
```
### `indego.sitemap` File
public static final String BINDING_ID = "boschindego";
// List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_INDEGO = new ThingTypeUID(BINDING_ID, "indego");
// List of all Channel ids
public static final String GARDEN_SIZE = "gardenSize";
public static final String GARDEN_MAP = "gardenMap";
- public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_INDEGO);
+ public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_INDEGO);
+
+ // Bosch SingleKey ID OAuth2
+ private static final String BSK_BASE_URI = "https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/";
+ public static final String BSK_CLIENT_ID = "65bb8c9d-1070-4fb4-aa95-853618acc876";
+ public static final String BSK_AUTH_URI = BSK_BASE_URI + "authorize";
+ public static final String BSK_TOKEN_URI = BSK_BASE_URI + "token";
+ public static final String BSK_REDIRECT_URI = "com.bosch.indegoconnect://login";
+ public static final String BSK_SCOPE = "openid offline_access https://prodindego.onmicrosoft.com/indego-mobile-api/Indego.Mower.User";
}
*/
package org.openhab.binding.boschindego.internal;
-import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.THING_TYPE_INDEGO;
+import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
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.BoschAccountHandler;
import org.openhab.binding.boschindego.internal.handler.BoschIndegoHandler;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
* handlers.
*
* @author Jonas Fleck - Initial contribution
+ * @author Jacob Laursen - Replaced authorization by OAuth2
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.boschindego")
public class BoschIndegoHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient;
+ private final OAuthFactory oAuthFactory;
private final BoschIndegoTranslationProvider translationProvider;
private final TimeZoneProvider timeZoneProvider;
@Activate
public BoschIndegoHandlerFactory(@Reference HttpClientFactory httpClientFactory,
- final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider,
- final @Reference TimeZoneProvider timeZoneProvider, ComponentContext componentContext) {
+ final @Reference OAuthFactory oAuthFactory, final @Reference TranslationProvider i18nProvider,
+ final @Reference LocaleProvider localeProvider, final @Reference TimeZoneProvider timeZoneProvider,
+ ComponentContext componentContext) {
super.activate(componentContext);
this.httpClient = httpClientFactory.getCommonHttpClient();
+ this.oAuthFactory = oAuthFactory;
this.translationProvider = new BoschIndegoTranslationProvider(i18nProvider, localeProvider);
this.timeZoneProvider = timeZoneProvider;
}
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
- if (THING_TYPE_INDEGO.equals(thingTypeUID)) {
+ if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
+ return new BoschAccountHandler((Bridge) thing, httpClient, oAuthFactory);
+ } else if (THING_TYPE_INDEGO.equals(thingTypeUID)) {
return new BoschIndegoHandler(thing, httpClient, translationProvider, timeZoneProvider);
}
*/
package org.openhab.binding.boschindego.internal;
-import java.net.URI;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Base64;
+import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
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.ErrorResponse;
-import org.openhab.binding.boschindego.internal.dto.response.LocationWeatherResponse;
-import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse;
-import org.openhab.binding.boschindego.internal.dto.response.PredictiveLastCuttingResponse;
-import org.openhab.binding.boschindego.internal.dto.response.PredictiveNextCuttingResponse;
+import org.openhab.binding.boschindego.internal.dto.response.Mower;
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.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthException;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.library.types.RawType;
+import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.
+ * Controller for communicating with a Bosch Indego services.
*
* @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";
+ protected static final String SERIAL_NUMBER_SUBPATH = "alms/";
+
+ private static final String BASE_URL = "https://api.indego-cloud.iot.bosch-si.com/api/v1/";
private static final String CONTENT_TYPE_HEADER = "application/json";
+ private static final String BEARER = "Bearer ";
+
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();
+ private final OAuthClientService oAuthClientService;
+ private final String userAgent;
/**
* Initialize the controller instance.
*
- * @param username the username for authenticating
- * @param password the password
+ * @param httpClient the HttpClient for communicating with the service
+ * @param oAuthClientService the OAuthClientService for authorization
*/
- public IndegoController(HttpClient httpClient, String username, String password) {
+ public IndegoController(HttpClient httpClient, OAuthClientService oAuthClientService) {
this.httpClient = httpClient;
- basicAuthenticationHeader = "Basic "
- + Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
+ this.oAuthClientService = oAuthClientService;
+ userAgent = "openHAB " + FrameworkUtil.getBundle(this.getClass()).getVersion().toString();
}
/**
- * Authenticate with server and store session context and serial number.
- *
+ * Gets serial numbers of all the associated Indego devices.
+ *
+ * @return the serial numbers of the devices
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
- private void authenticate() throws IndegoAuthenticationException, IndegoException {
- int status = 0;
- 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");
- }
+ public Collection<String> getSerialNumbers() throws IndegoAuthenticationException, IndegoException {
+ Mower[] mowers = getRequest(SERIAL_NUMBER_SUBPATH, Mower[].class);
- ContentResponse response = sendRequest(request);
- 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", status);
- }
- 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",
- status);
- }
- session = new IndegoSession(authenticationResponse.contextId, authenticationResponse.serialNumber,
- getContextExpirationTimeFromCookie());
- logger.debug("Initialized session {}", session);
- } catch (JsonParseException e) {
- throw new IndegoInvalidResponseException("Error parsing AuthenticationResponse", e, status);
- } 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;
- });
+ return Arrays.stream(mowers).map(m -> m.serialNumber).toList();
}
- /**
- * Deauthenticate session. This method should be called as part of cleanup to reduce
- * lingering sessions. This can potentially avoid killed sessions in situation with
- * multiple clients (e.g. openHAB and mobile app) if restrictions on concurrent
- * number of sessions would be put on the service.
- *
- * @throws IndegoException if any communication or parsing error occurred
- */
- public void deauthenticate() throws IndegoException {
- if (session.isValid()) {
- deleteRequest("authenticate");
- session.invalidate();
+ private String getAuthorizationUrl() {
+ try {
+ return oAuthClientService.getAuthorizationUrl(BSK_REDIRECT_URI, BSK_SCOPE, null);
+ } catch (OAuthException e) {
+ return "";
}
}
- /**
- * 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 IndegoTimeoutException if device cannot be reached (gateway timeout error)
- * @throws IndegoException if any communication or parsing error occurred
- */
- private <T> T getRequestWithAuthentication(String path, Class<? extends T> dtoClass)
- throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
- if (!session.isValid()) {
- authenticate();
- }
+ private String getAuthorizationHeader() throws IndegoException {
+ final AccessTokenResponse accessTokenResponse;
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);
+ accessTokenResponse = oAuthClientService.getAccessTokenResponse();
+ } catch (OAuthException | OAuthResponseException e) {
+ logger.debug("Error fetching access token: {}", e.getMessage(), e);
+ throw new IndegoAuthenticationException(
+ "Error fetching access token. Invalid authcode? Please generate a new one -> "
+ + getAuthorizationUrl(),
+ e);
+ } catch (IOException e) {
+ throw new IndegoException("An unexpected IOException occurred: " + e.getMessage(), e);
+ }
+ if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null
+ || accessTokenResponse.getAccessToken().isEmpty()) {
+ throw new IndegoAuthenticationException(
+ "No access token. Is this thing authorized? -> " + getAuthorizationUrl());
+ }
+ if (accessTokenResponse.getRefreshToken() == null || accessTokenResponse.getRefreshToken().isEmpty()) {
+ throw new IndegoAuthenticationException("No refresh token. Please reauthorize -> " + getAuthorizationUrl());
}
+
+ return BEARER + accessTokenResponse.getAccessToken();
}
/**
* @throws IndegoTimeoutException if device cannot be reached (gateway timeout error)
* @throws IndegoException if any communication or parsing error occurred
*/
- private <T> T getRequest(String path, Class<? extends T> dtoClass)
+ protected <T> T getRequest(String path, Class<? extends T> dtoClass)
throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
int status = 0;
try {
- Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET).header(CONTEXT_HEADER_NAME,
- session.getContextId());
+ Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET)
+ .header(HttpHeader.AUTHORIZATION, getAuthorizationHeader()).agent(userAgent);
if (logger.isTraceEnabled()) {
logger.trace("GET request for {}", BASE_URL + path);
}
}
if (status == HttpStatus.UNAUTHORIZED_401) {
// This will currently not happen because "WWW-Authenticate" header is missing; see below.
- throw new IndegoAuthenticationException("Context rejected");
+ throw new IndegoAuthenticationException("Unauthorized");
}
if (status == HttpStatus.GATEWAY_TIMEOUT_504) {
throw new IndegoTimeoutException("Gateway timeout");
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.
+ * The service may 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 IndegoAuthenticationException("Unauthorized", e);
}
}
throw new IndegoException(e);
}
}
- /**
- * Wraps {@link #getRawRequest(String)} into an authenticated session.
- *
- * @param path the relative path to which the request should be sent
- * @return the raw data from the response
- * @throws IndegoAuthenticationException if request was rejected as unauthorized
- * @throws IndegoException if any communication or parsing error occurred
- */
- private RawType getRawRequestWithAuthentication(String path) throws IndegoAuthenticationException, IndegoException {
- if (!session.isValid()) {
- authenticate();
- }
- try {
- logger.debug("Session {} valid, skipping authentication", session);
- return getRawRequest(path);
- } catch (IndegoAuthenticationException e) {
- if (logger.isTraceEnabled()) {
- logger.trace("Context rejected", e);
- } else {
- logger.debug("Context rejected: {}", e.getMessage());
- }
- session.invalidate();
- authenticate();
- return getRawRequest(path);
- }
- }
-
/**
* Sends a GET request to the server and returns the raw response.
*
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
- private RawType getRawRequest(String path) throws IndegoAuthenticationException, IndegoException {
+ protected RawType getRawRequest(String path) throws IndegoAuthenticationException, IndegoException {
int status = 0;
try {
- Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET).header(CONTEXT_HEADER_NAME,
- session.getContextId());
+ Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET)
+ .header(HttpHeader.AUTHORIZATION, getAuthorizationHeader()).agent(userAgent);
if (logger.isTraceEnabled()) {
logger.trace("GET request for {}", BASE_URL + path);
}
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
- private void putRequestWithAuthentication(String path, Object requestDto)
+ protected void putRequestWithAuthentication(String path, Object requestDto)
throws IndegoAuthenticationException, IndegoException {
- if (!session.isValid()) {
- authenticate();
- }
- try {
- logger.debug("Session {} valid, skipping authentication", session);
- putPostRequest(HttpMethod.PUT, path, requestDto);
- } catch (IndegoAuthenticationException e) {
- if (logger.isTraceEnabled()) {
- logger.trace("Context rejected", e);
- } else {
- logger.debug("Context rejected: {}", e.getMessage());
- }
- session.invalidate();
- authenticate();
- putPostRequest(HttpMethod.PUT, path, requestDto);
- }
+ putPostRequest(HttpMethod.PUT, path, requestDto);
}
/**
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
- private void postRequestWithAuthentication(String path) throws IndegoAuthenticationException, IndegoException {
- if (!session.isValid()) {
- authenticate();
- }
- try {
- logger.debug("Session {} valid, skipping authentication", session);
- putPostRequest(HttpMethod.POST, path, null);
- } catch (IndegoAuthenticationException e) {
- if (logger.isTraceEnabled()) {
- logger.trace("Context rejected", e);
- } else {
- logger.debug("Context rejected: {}", e.getMessage());
- }
- session.invalidate();
- authenticate();
- putPostRequest(HttpMethod.POST, path, null);
- }
+ protected void postRequest(String path) throws IndegoAuthenticationException, IndegoException {
+ putPostRequest(HttpMethod.POST, path, null);
}
/**
* @throws IndegoAuthenticationException if request was rejected as unauthorized
* @throws IndegoException if any communication or parsing error occurred
*/
- private void putPostRequest(HttpMethod method, String path, @Nullable Object requestDto)
+ protected void putPostRequest(HttpMethod method, String path, @Nullable Object requestDto)
throws IndegoAuthenticationException, IndegoException {
try {
Request request = httpClient.newRequest(BASE_URL + path).method(method)
- .header(CONTEXT_HEADER_NAME, session.getContextId())
- .header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_HEADER);
+ .header(HttpHeader.AUTHORIZATION, getAuthorizationHeader())
+ .header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_HEADER).agent(userAgent);
if (requestDto != null) {
String payload = gson.toJson(requestDto);
request.content(new StringContentProvider(payload));
}
}
- /**
- * Sends a DELETE request to the server.
- *
- * @param path the relative path to which the request should be sent
- * @throws IndegoException if any communication or parsing error occurred
- */
- private void deleteRequest(String path) throws IndegoException {
- try {
- Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.DELETE)
- .header(CONTEXT_HEADER_NAME, session.getContextId());
- if (logger.isTraceEnabled()) {
- logger.trace("DELETE request for {}", BASE_URL + path);
- }
- ContentResponse response = sendRequest(request);
- int status = response.getStatus();
- if (!HttpStatus.isSuccess(status)) {
- throw new IndegoException("The request failed with error: " + status);
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new IndegoException(e);
- } catch (TimeoutException | ExecutionException e) {
- throw new IndegoException(e);
- }
- }
-
/**
* Send request. This method exists for the purpose of avoiding multiple calls to
* the server at the same time.
* @throws TimeoutException if send times out
* @throws ExecutionException if execution fails
*/
- private synchronized ContentResponse sendRequest(Request request)
+ protected 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 synchronized 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 device state from the server. This overload will return when the state
- * has changed, or the timeout has been reached.
- *
- * @param timeout Maximum time to wait for response
- * @return the device state
- * @throws IndegoAuthenticationException if request was rejected as unauthorized
- * @throws IndegoException if any communication or parsing error occurred
- */
- public DeviceStateResponse getState(Duration timeout) throws IndegoAuthenticationException, IndegoException {
- return getRequestWithAuthentication(
- SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/state?longpoll=true&timeout=" + timeout.getSeconds(),
- DeviceStateResponse.class);
- }
-
- /**
- * Queries the device operating data from the server.
- * Server will request this directly from the device, so operation might be slow.
- *
- * @return the device state
- * @throws IndegoAuthenticationException if request was rejected as unauthorized
- * @throws IndegoTimeoutException if device cannot be reached (gateway timeout error)
- * @throws IndegoException if any communication or parsing error occurred
- */
- public OperatingDataResponse getOperatingData()
- throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
- return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/operatingData",
- OperatingDataResponse.class);
- }
-
- /**
- * Queries the map generated by the device from the server.
- *
- * @return the garden map
- * @throws IndegoAuthenticationException if request was rejected as unauthorized
- * @throws IndegoException if any communication or parsing error occurred
- */
- public RawType getMap() throws IndegoAuthenticationException, IndegoException {
- return getRawRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/map");
- }
-
- /**
- * 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 {
- return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive",
- PredictiveStatus.class).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 last cutting as {@link Instant}.
- *
- * @return predictive last cutting
- * @throws IndegoAuthenticationException if request was rejected as unauthorized
- * @throws IndegoException if any communication or parsing error occurred
- */
- public @Nullable Instant getPredictiveLastCutting() throws IndegoAuthenticationException, IndegoException {
- try {
- return getRequestWithAuthentication(
- SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/lastcutting",
- PredictiveLastCuttingResponse.class).getLastCutting();
- } catch (IndegoInvalidResponseException e) {
- if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
- return null;
- }
- throw e;
- }
- }
-
- /**
- * 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 @Nullable Instant getPredictiveNextCutting() throws IndegoAuthenticationException, IndegoException {
- try {
- return getRequestWithAuthentication(
- SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/nextcutting",
- PredictiveNextCuttingResponse.class).getNextCutting();
- } catch (IndegoInvalidResponseException e) {
- if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
- return null;
- }
- throw e;
- }
- }
-
- /**
- * 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 {
- return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/calendar",
- DeviceCalendarResponse.class);
- }
-
- /**
- * 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);
- }
-
- /**
- * Request map position updates for the next ({@link count} * {@link interval}) number of seconds.
- *
- * @param count Number of updates
- * @param interval Number of seconds between updates
- * @throws IndegoAuthenticationException if request was rejected as unauthorized
- * @throws IndegoException if any communication or parsing error occurred
- */
- public void requestPosition(int count, int interval) throws IndegoAuthenticationException, IndegoException {
- postRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/requestPosition?count=" + count
- + "&interval=" + interval);
- }
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+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.SetStateRequest;
+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.OperatingDataResponse;
+import org.openhab.binding.boschindego.internal.dto.response.PredictiveLastCuttingResponse;
+import org.openhab.binding.boschindego.internal.dto.response.PredictiveNextCuttingResponse;
+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.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.library.types.RawType;
+
+/**
+ * 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's 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.
+ *
+ * @see <a href=
+ * "https://github.com/zazaz-de/iot-device-bosch-indego-controller">zazaz-de/iot-device-bosch-indego-controller</a>
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class IndegoDeviceController extends IndegoController {
+
+ private String serialNumber;
+
+ /**
+ * Initialize the controller instance.
+ *
+ * @param httpClient the HttpClient for communicating with the service
+ * @param oAuthClientService the OAuthClientService for authorization
+ * @param serialNumber the serial number of the device instance
+ */
+ public IndegoDeviceController(HttpClient httpClient, OAuthClientService oAuthClientService, String serialNumber) {
+ super(httpClient, oAuthClientService);
+ if (serialNumber.isBlank()) {
+ throw new IllegalArgumentException("Serial number must be provided");
+ }
+ this.serialNumber = serialNumber;
+ }
+
+ /**
+ * 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 getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/state", DeviceStateResponse.class);
+ }
+
+ /**
+ * Queries the device state from the server. This overload will return when the state
+ * has changed, or the timeout has been reached.
+ *
+ * @param timeout maximum time to wait for response
+ * @return the device state
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public DeviceStateResponse getState(Duration timeout) throws IndegoAuthenticationException, IndegoException {
+ return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/state?longpoll=true&timeout=" + timeout.getSeconds(),
+ DeviceStateResponse.class);
+ }
+
+ /**
+ * Queries the device operating data from the server.
+ * Server will request this directly from the device, so operation might be slow.
+ *
+ * @return the device state
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoTimeoutException if device cannot be reached (gateway timeout error)
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public OperatingDataResponse getOperatingData()
+ throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
+ return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/operatingData", OperatingDataResponse.class);
+ }
+
+ /**
+ * Queries the map generated by the device from the server.
+ *
+ * @return the garden map
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public RawType getMap() throws IndegoAuthenticationException, IndegoException {
+ return getRawRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/map");
+ }
+
+ /**
+ * 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 = getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/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 + serialNumber + "/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 getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/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 getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/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 + serialNumber + "/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 {
+ return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive", PredictiveStatus.class).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 + serialNumber + "/predictive", status);
+ }
+
+ /**
+ * Queries predictive last cutting as {@link Instant}.
+ *
+ * @return predictive last cutting
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public @Nullable Instant getPredictiveLastCutting() throws IndegoAuthenticationException, IndegoException {
+ try {
+ return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/lastcutting",
+ PredictiveLastCuttingResponse.class).getLastCutting();
+ } catch (IndegoInvalidResponseException e) {
+ if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
+ return null;
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * 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 @Nullable Instant getPredictiveNextCutting() throws IndegoAuthenticationException, IndegoException {
+ try {
+ return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/nextcutting",
+ PredictiveNextCuttingResponse.class).getNextCutting();
+ } catch (IndegoInvalidResponseException e) {
+ if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
+ return null;
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * 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 {
+ return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/calendar", DeviceCalendarResponse.class);
+ }
+
+ /**
+ * 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 + serialNumber + "/predictive/calendar", calendar);
+ }
+
+ /**
+ * Request map position updates for the next ({@link count} * {@link interval}) number of seconds.
+ *
+ * @param count number of updates
+ * @param interval number of seconds between updates
+ * @throws IndegoAuthenticationException if request was rejected as unauthorized
+ * @throws IndegoException if any communication or parsing error occurred
+ */
+ public void requestPosition(int count, int interval) throws IndegoAuthenticationException, IndegoException {
+ postRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/requestPosition?count=" + count + "&interval=" + interval);
+ }
+}
+++ /dev/null
-/**
- * Copyright (c) 2010-2023 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: <contextId>" and
- * "Cookie: BOSCH_INDEGO_SSO=<contextId>").
- *
- * @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);
- }
-}
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.
*/
@NonNullByDefault
public class BoschIndegoConfiguration {
- public @Nullable String username;
- public @Nullable String password;
+ public String serialNumber = "";
public long refresh = 180;
public long stateActiveRefresh = 30;
public long cuttingTimeRefresh = 60;
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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.console;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
+import org.openhab.binding.boschindego.internal.handler.BoschAccountHandler;
+import org.openhab.core.io.console.Console;
+import org.openhab.core.io.console.ConsoleCommandCompleter;
+import org.openhab.core.io.console.StringsCompleter;
+import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
+import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link BoschIndegoCommandExtension} is responsible for handling console commands
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = ConsoleCommandExtension.class)
+public class BoschIndegoCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter {
+
+ private static final String AUTHORIZE = "authorize";
+ private static final StringsCompleter SUBCMD_COMPLETER = new StringsCompleter(List.of(AUTHORIZE), false);
+
+ private final ThingRegistry thingRegistry;
+
+ @Activate
+ public BoschIndegoCommandExtension(final @Reference ThingRegistry thingRegistry) {
+ super(BoschIndegoBindingConstants.BINDING_ID, "Interact with the Bosch Indego binding.");
+ this.thingRegistry = thingRegistry;
+ }
+
+ @Override
+ public void execute(String[] args, Console console) {
+ if (args.length != 2 || !AUTHORIZE.equals(args[0])) {
+ printUsage(console);
+ return;
+ }
+
+ for (Thing thing : thingRegistry.getAll()) {
+ ThingHandler thingHandler = thing.getHandler();
+ if (thingHandler instanceof BoschAccountHandler accountHandler) {
+ try {
+ accountHandler.authorize(args[1]);
+ } catch (IndegoAuthenticationException e) {
+ console.println("Authorization error: " + e.getMessage());
+ }
+ }
+ }
+ }
+
+ @Override
+ public List<String> getUsages() {
+ return Arrays.asList(buildCommandUsage(AUTHORIZE, "authorize by authorization code"));
+ }
+
+ @Override
+ public @Nullable ConsoleCommandCompleter getCompleter() {
+ return this;
+ }
+
+ @Override
+ public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
+ if (cursorArgumentIndex <= 0) {
+ return SUBCMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
+ }
+ return false;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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.discovery;
+
+import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
+
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
+import org.openhab.binding.boschindego.internal.handler.BoschAccountHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link IndegoDiscoveryService} is responsible for discovering Indego mowers.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class IndegoDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+
+ private static final int TIMEOUT_SECONDS = 60;
+
+ private final Logger logger = LoggerFactory.getLogger(IndegoDiscoveryService.class);
+
+ private @NonNullByDefault({}) BoschAccountHandler accountHandler;
+
+ public IndegoDiscoveryService() {
+ super(Set.of(THING_TYPE_ACCOUNT), TIMEOUT_SECONDS, false);
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return accountHandler;
+ }
+
+ @Override
+ public void setThingHandler(ThingHandler handler) {
+ if (handler instanceof BoschAccountHandler accountHandler) {
+ this.accountHandler = accountHandler;
+ }
+ }
+
+ @Override
+ public Set<ThingTypeUID> getSupportedThingTypes() {
+ return Set.of(THING_TYPE_INDEGO);
+ }
+
+ @Override
+ public void startScan() {
+ try {
+ Collection<String> serialNumbers = accountHandler.getSerialNumbers();
+
+ ThingUID bridgeUID = accountHandler.getThing().getUID();
+ for (String serialNumber : serialNumbers) {
+ ThingUID thingUID = new ThingUID(THING_TYPE_INDEGO, bridgeUID, serialNumber);
+ DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
+ .withProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber).withBridge(bridgeUID)
+ .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER)
+ .withLabel("Indego (" + serialNumber + ")").build();
+
+ thingDiscovered(discoveryResult);
+ }
+ } catch (IndegoException e) {
+ logger.debug("Failed to retrieve serial numbers: {}", e.getMessage());
+ }
+ }
+
+ @Override
+ protected synchronized void stopScan() {
+ super.stopScan();
+ removeOlderResults(getTimestampOfLastScan());
+ }
+
+ @Override
+ public void deactivate() {
+ removeOlderResults(Instant.now().getEpochSecond());
+ }
+}
+++ /dev/null
-/**
- * Copyright (c) 2010-2023 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;
-}
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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;
+
+/**
+ * Mower serial number and status.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class Mower {
+
+ @SerializedName("alm_sn")
+ public String serialNumber;
+
+ @SerializedName("alm_status")
+ public int status;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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.handler;
+
+import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.boschindego.internal.IndegoController;
+import org.openhab.binding.boschindego.internal.discovery.IndegoDiscoveryService;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthException;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link BoschAccountHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class BoschAccountHandler extends BaseBridgeHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(BoschAccountHandler.class);
+ private final OAuthFactory oAuthFactory;
+
+ private OAuthClientService oAuthClientService;
+ private IndegoController controller;
+
+ public BoschAccountHandler(Bridge bridge, HttpClient httpClient, OAuthFactory oAuthFactory) {
+ super(bridge);
+
+ this.oAuthFactory = oAuthFactory;
+
+ oAuthClientService = oAuthFactory.createOAuthClientService(getThing().getUID().getAsString(), BSK_TOKEN_URI,
+ BSK_AUTH_URI, BSK_CLIENT_ID, null, BSK_SCOPE, false);
+ controller = new IndegoController(httpClient, oAuthClientService);
+ }
+
+ @Override
+ public void initialize() {
+ updateStatus(ThingStatus.UNKNOWN);
+
+ scheduler.execute(() -> {
+ try {
+ AccessTokenResponse accessTokenResponse = this.oAuthClientService.getAccessTokenResponse();
+ if (accessTokenResponse == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
+ "@text/offline.conf-error.oauth2-unauthorized");
+ } else {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ } catch (OAuthException | OAuthResponseException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
+ "@text/offline.conf-error.oauth2-unauthorized");
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+ "@text/offline.comm-error.oauth2-authorization-failed");
+ }
+ });
+ }
+
+ @Override
+ public void dispose() {
+ oAuthFactory.ungetOAuthService(this.getThing().getUID().getAsString());
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ }
+
+ @Override
+ public Collection<Class<? extends ThingHandlerService>> getServices() {
+ return List.of(IndegoDiscoveryService.class);
+ }
+
+ public void authorize(String authCode) throws IndegoAuthenticationException {
+ logger.info("Attempting to authorize using authorization code");
+
+ try {
+ oAuthClientService.getAccessTokenResponseByAuthorizationCode(authCode, BSK_REDIRECT_URI);
+ } catch (OAuthException | OAuthResponseException | IOException e) {
+ throw new IndegoAuthenticationException("Failed to authorize by authorization code " + authCode, e);
+ }
+
+ logger.info("Authorization completed successfully");
+
+ updateStatus(ThingStatus.ONLINE);
+ }
+
+ public OAuthClientService getOAuthClientService() {
+ return oAuthClientService;
+ }
+
+ public Collection<String> getSerialNumbers() throws IndegoException {
+ return controller.getSerialNumbers();
+ }
+}
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.boschindego.internal.BoschIndegoTranslationProvider;
import org.openhab.binding.boschindego.internal.DeviceStatus;
-import org.openhab.binding.boschindego.internal.IndegoController;
+import org.openhab.binding.boschindego.internal.IndegoDeviceController;
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.IndegoException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
private final BoschIndegoTranslationProvider translationProvider;
private final TimeZoneProvider timeZoneProvider;
- private @NonNullByDefault({}) IndegoController controller;
+ private @NonNullByDefault({}) OAuthClientService oAuthClientService;
+ private @NonNullByDefault({}) IndegoDeviceController controller;
private @Nullable ScheduledFuture<?> statePollFuture;
private @Nullable ScheduledFuture<?> cuttingTimePollFuture;
private @Nullable ScheduledFuture<?> cuttingTimeFuture;
- private boolean propertiesInitialized;
private Optional<Integer> previousStateCode = Optional.empty();
private @Nullable RawType cachedMap;
private Instant cachedMapTimestamp = Instant.MIN;
@Override
public void initialize() {
- logger.debug("Initializing Indego handler");
BoschIndegoConfiguration config = getConfigAs(BoschIndegoConfiguration.class);
stateInactiveRefreshIntervalSeconds = (int) config.refresh;
stateActiveRefreshIntervalSeconds = (int) config.stateActiveRefresh;
- String username = config.username;
- String password = config.password;
- if (username == null || username.isBlank()) {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
- "@text/offline.conf-error.missing-username");
+ "@text/offline.conf-error.missing-bridge");
return;
}
- if (password == null || password.isBlank()) {
+
+ ThingHandler handler = bridge.getHandler();
+ if (handler instanceof BoschAccountHandler accountHandler) {
+ this.oAuthClientService = accountHandler.getOAuthClientService();
+ } else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
- "@text/offline.conf-error.missing-password");
+ "@text/offline.conf-error.missing-bridge");
return;
}
- controller = new IndegoController(httpClient, username, password);
+ this.updateProperty(Thing.PROPERTY_SERIAL_NUMBER, config.serialNumber);
+
+ controller = new IndegoDeviceController(httpClient, oAuthClientService, config.serialNumber);
updateStatus(ThingStatus.UNKNOWN);
previousStateCode = Optional.empty();
- rescheduleStatePoll(0, stateInactiveRefreshIntervalSeconds);
+ rescheduleStatePoll(0, stateInactiveRefreshIntervalSeconds, false);
this.cuttingTimePollFuture = scheduler.scheduleWithFixedDelay(this::refreshCuttingTimesWithExceptionHandling, 0,
config.cuttingTimeRefresh, TimeUnit.MINUTES);
}
- private boolean rescheduleStatePoll(int delaySeconds, int refreshIntervalSeconds) {
+ @Override
+ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+ if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
+ && getThing().getStatusInfo().getStatus() == ThingStatus.OFFLINE) {
+ // Trigger immediate state refresh upon authorization success.
+ rescheduleStatePoll(0, stateInactiveRefreshIntervalSeconds, true);
+ } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ }
+ }
+
+ private boolean rescheduleStatePoll(int delaySeconds, int refreshIntervalSeconds, boolean force) {
ScheduledFuture<?> statePollFuture = this.statePollFuture;
if (statePollFuture != null) {
- if (refreshIntervalSeconds == currentRefreshIntervalSeconds) {
+ if (!force && refreshIntervalSeconds == currentRefreshIntervalSeconds) {
// No change.
return false;
}
- statePollFuture.cancel(false);
+ statePollFuture.cancel(force);
}
logger.debug("Scheduling state refresh job with {}s interval and {}s delay", refreshIntervalSeconds,
delaySeconds);
@Override
public void dispose() {
- logger.debug("Disposing Indego handler");
ScheduledFuture<?> pollFuture = this.statePollFuture;
if (pollFuture != null) {
pollFuture.cancel(true);
pollFuture.cancel(true);
}
this.cuttingTimeFuture = null;
-
- scheduler.execute(() -> {
- try {
- controller.deauthenticate();
- } catch (IndegoException e) {
- logger.debug("Deauthentication failed", e);
- }
- });
}
@Override
try {
refreshState();
} catch (IndegoAuthenticationException e) {
+ logger.warn("Failed to authenticate: {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.authentication-failure");
} catch (IndegoTimeoutException e) {
}
private void refreshState() throws IndegoAuthenticationException, IndegoException {
- if (!propertiesInitialized) {
- getThing().setProperty(Thing.PROPERTY_SERIAL_NUMBER, controller.getSerialNumber());
- propertiesInitialized = true;
- }
-
DeviceStateResponse state = controller.getState();
DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
updateState(state);
} else {
refreshIntervalSeconds = stateInactiveRefreshIntervalSeconds;
}
- if (rescheduleStatePoll(refreshIntervalSeconds, refreshIntervalSeconds)) {
+ if (rescheduleStatePoll(refreshIntervalSeconds, refreshIntervalSeconds, false)) {
// After job has been rescheduled, request operating data one last time on next poll.
// This is needed to update battery values after a charging cycle has completed.
operatingDataTimestamp = Instant.MIN;
# thing types
+thing-type.boschindego.account.label = SingleKey ID
+thing-type.boschindego.account.description = SingleKey ID account
thing-type.boschindego.indego.label = Bosch Indego
thing-type.boschindego.indego.description = Indego which supports the connect feature.
thing-type.config.boschindego.indego.cuttingTimeRefresh.label = Cutting Time Refresh Interval
thing-type.config.boschindego.indego.cuttingTimeRefresh.description = The number of minutes between refreshing last/next cutting time.
-thing-type.config.boschindego.indego.password.label = Password
-thing-type.config.boschindego.indego.password.description = Password for the Bosch Indego account.
thing-type.config.boschindego.indego.refresh.label = Idle Refresh Interval
thing-type.config.boschindego.indego.refresh.description = The number of seconds between refreshing device state when idle.
+thing-type.config.boschindego.indego.serialNumber.label = Serial Number
+thing-type.config.boschindego.indego.serialNumber.description = The serial number of the connected Indego mower.
thing-type.config.boschindego.indego.stateActiveRefresh.label = Active Refresh Interval
thing-type.config.boschindego.indego.stateActiveRefresh.description = The number of seconds between refreshing device state when active.
-thing-type.config.boschindego.indego.username.label = Username
-thing-type.config.boschindego.indego.username.description = Username for the Bosch Indego account.
# channel types
# 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-bridge = No bridge configured
+offline.conf-error.oauth2-unauthorized = Unauthorized
+offline.comm-error.oauth2-authorization-failed = Failed to authorize
+offline.comm-error.authentication-failure = Failed to authenticate with Bosch SingleKey ID
offline.comm-error.unreachable = Device is unreachable
-offline.conf-error.missing-password = Password missing
-offline.conf-error.missing-username = Username missing
# indego states
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+ <bridge-type id="account">
+ <label>SingleKey ID</label>
+ <description>SingleKey ID account</description>
+ </bridge-type>
+
<thing-type id="indego">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
<label>Bosch Indego</label>
<description>Indego which supports the connect feature.</description>
+
<channels>
<channel id="state" typeId="state"/>
<channel id="textualstate" typeId="textualstate"/>
<channel id="gardenSize" typeId="gardenSize"/>
<channel id="gardenMap" typeId="gardenMap"/>
</channels>
+
+ <representation-property>serialNumber</representation-property>
+
<config-description>
- <parameter name="username" type="text" required="true">
- <label>Username</label>
- <description>Username for the Bosch Indego account.</description>
- </parameter>
- <parameter name="password" type="text" required="true">
- <context>password</context>
- <label>Password</label>
- <description>Password for the Bosch Indego account.</description>
+ <parameter name="serialNumber" type="text" required="true">
+ <label>Serial Number</label>
+ <description>The serial number of the connected Indego mower.</description>
</parameter>
<parameter name="refresh" type="integer" min="60">
<label>Idle Refresh Interval</label>