mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[boschindego] Rewrite to avoid external dependencies (#12905)
* Rewrite to avoid external dependencies Fixes #12720 * Improve session handling * Avoid reauthorization for each command/poll * Further improve session handling * Refactor SSO cookie handling * Optimize getting DeviceStatus for unknown status code Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
parent
fd9fa722d3
commit
960be6bc83
@ -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"]
|
||||
```
|
||||
|
@ -14,35 +14,4 @@
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Bosch Indego Binding</name>
|
||||
|
||||
<properties>
|
||||
<dep.noembedding>httpclient-osgi,httpcore-osgi,commons-codec</dep.noembedding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>de.zazaz.iot.bosch.indego</groupId>
|
||||
<artifactId>bosch-indego-controller-lib</artifactId>
|
||||
<version>0.8</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient-osgi</artifactId>
|
||||
<version>4.5.5</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpcore-osgi</artifactId>
|
||||
<version>4.4.9</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.10</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -4,10 +4,6 @@
|
||||
|
||||
<feature name="openhab-binding-boschindego" description="Bosch Indego Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature dependency="true">openhab.tp-jackson</feature>
|
||||
<bundle dependency="true">mvn:org.apache.httpcomponents/httpcore-osgi/4.4.9</bundle>
|
||||
<bundle dependency="true">mvn:org.apache.httpcomponents/httpclient-osgi/4.5.5</bundle>
|
||||
<bundle dependency="true">mvn:commons-codec/commons-codec/1.10</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.boschindego/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
|
@ -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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_INDEGO);
|
||||
}
|
||||
|
@ -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<ThingTypeUID> 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;
|
||||
|
@ -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<Integer, DeviceStatus> 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;
|
||||
}
|
||||
}
|
@ -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> 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> 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);
|
||||
}
|
||||
}
|
@ -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: <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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<DeviceCommand> 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<String, Object> configurationParameters) {
|
||||
super.handleConfigurationUpdate(configurationParameters);
|
||||
reschedule();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
reschedule();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -48,7 +48,7 @@
|
||||
<item-type>Number</item-type>
|
||||
<label>Error Code</label>
|
||||
<description>0 = no error</description>
|
||||
<state readOnly="false"></state>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
<channel-type id="statecode" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
@ -78,7 +78,11 @@
|
||||
<option value="773">Returning to Dock - Battery temp range</option>
|
||||
<option value="774">Returning to Dock</option>
|
||||
<option value="775">Returning to Dock - Lawn complete</option>
|
||||
<option value="775">Returning to Dock - Relocalising</option>
|
||||
<option value="776">Returning to Dock - Relocalising</option>
|
||||
<option value="1025">Diagnostic mode</option>
|
||||
<option value="1026">End of life</option>
|
||||
<option value="1281">Software update</option>
|
||||
<option value="64513">Docked</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
Loading…
Reference in New Issue
Block a user