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