2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.boschindego.internal;
15 import java.time.Duration;
16 import java.time.Instant;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.eclipse.jetty.client.HttpClient;
21 import org.eclipse.jetty.http.HttpStatus;
22 import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
23 import org.openhab.binding.boschindego.internal.dto.PredictiveAdjustment;
24 import org.openhab.binding.boschindego.internal.dto.PredictiveStatus;
25 import org.openhab.binding.boschindego.internal.dto.request.SetStateRequest;
26 import org.openhab.binding.boschindego.internal.dto.response.DeviceCalendarResponse;
27 import org.openhab.binding.boschindego.internal.dto.response.DevicePropertiesResponse;
28 import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse;
29 import org.openhab.binding.boschindego.internal.dto.response.LocationWeatherResponse;
30 import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse;
31 import org.openhab.binding.boschindego.internal.dto.response.PredictiveLastCuttingResponse;
32 import org.openhab.binding.boschindego.internal.dto.response.PredictiveNextCuttingResponse;
33 import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
34 import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
35 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
36 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
37 import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
38 import org.openhab.core.auth.client.oauth2.OAuthClientService;
39 import org.openhab.core.library.types.RawType;
42 * Controller for communicating with a Bosch Indego device through Bosch services.
43 * This class provides methods for retrieving state information as well as controlling
46 * The implementation is based on zazaz-de's iot-device-bosch-indego-controller, but
47 * rewritten from scratch to use Jetty HTTP client for HTTP communication and GSON for
48 * JSON parsing. Thanks to Oliver Schünemann for providing the original implementation.
51 * "https://github.com/zazaz-de/iot-device-bosch-indego-controller">zazaz-de/iot-device-bosch-indego-controller</a>
53 * @author Jacob Laursen - Initial contribution
56 public class IndegoDeviceController extends IndegoController {
58 private String serialNumber;
61 * Initialize the controller instance.
63 * @param httpClient the HttpClient for communicating with the service
64 * @param oAuthClientService the OAuthClientService for authorization
65 * @param serialNumber the serial number of the device instance
67 public IndegoDeviceController(HttpClient httpClient, OAuthClientService oAuthClientService, String serialNumber) {
68 super(httpClient, oAuthClientService);
69 if (serialNumber.isBlank()) {
70 throw new IllegalArgumentException("Serial number must be provided");
72 this.serialNumber = serialNumber;
76 * Queries the serial number and device service properties from the server.
78 * @return the device serial number and properties
79 * @throws IndegoAuthenticationException if request was rejected as unauthorized
80 * @throws IndegoException if any communication or parsing error occurred
82 public DevicePropertiesResponse getDeviceProperties() throws IndegoAuthenticationException, IndegoException {
83 return super.getDeviceProperties(serialNumber);
87 * Queries the device state from the server.
89 * @return the device state
90 * @throws IndegoAuthenticationException if request was rejected as unauthorized
91 * @throws IndegoException if any communication or parsing error occurred
93 public DeviceStateResponse getState() throws IndegoAuthenticationException, IndegoException {
94 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/state", DeviceStateResponse.class);
98 * Queries the device state from the server. This overload will return when the state
99 * has changed, or the timeout has been reached.
101 * @param timeout maximum time to wait for response
102 * @return the device state
103 * @throws IndegoAuthenticationException if request was rejected as unauthorized
104 * @throws IndegoException if any communication or parsing error occurred
106 public DeviceStateResponse getState(Duration timeout) throws IndegoAuthenticationException, IndegoException {
107 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/state?longpoll=true&timeout=" + timeout.getSeconds(),
108 DeviceStateResponse.class);
112 * Queries the device operating data from the server.
113 * Server will request this directly from the device, so operation might be slow.
115 * @return the device state
116 * @throws IndegoAuthenticationException if request was rejected as unauthorized
117 * @throws IndegoTimeoutException if device cannot be reached (gateway timeout error)
118 * @throws IndegoException if any communication or parsing error occurred
120 public OperatingDataResponse getOperatingData()
121 throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
122 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/operatingData", OperatingDataResponse.class);
126 * Queries the map generated by the device from the server.
128 * @return the garden map
129 * @throws IndegoAuthenticationException if request was rejected as unauthorized
130 * @throws IndegoException if any communication or parsing error occurred
132 public RawType getMap() throws IndegoAuthenticationException, IndegoException {
133 return getRawRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/map");
137 * Queries the calendar.
139 * @return the calendar
140 * @throws IndegoAuthenticationException if request was rejected as unauthorized
141 * @throws IndegoException if any communication or parsing error occurred
143 public DeviceCalendarResponse getCalendar() throws IndegoAuthenticationException, IndegoException {
144 DeviceCalendarResponse calendar = getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/calendar",
145 DeviceCalendarResponse.class);
150 * Sends a command to the Indego device.
152 * @param command the control command to send to the device
153 * @throws IndegoAuthenticationException if request was rejected as unauthorized
154 * @throws IndegoInvalidCommandException if the command was not processed correctly
155 * @throws IndegoException if any communication or parsing error occurred
157 public void sendCommand(DeviceCommand command)
158 throws IndegoAuthenticationException, IndegoInvalidCommandException, IndegoException {
159 SetStateRequest request = new SetStateRequest();
160 request.state = command.getActionCode();
161 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/state", request);
165 * Queries the predictive weather forecast.
167 * @return the weather forecast DTO
168 * @throws IndegoAuthenticationException if request was rejected as unauthorized
169 * @throws IndegoException if any communication or parsing error occurred
171 public LocationWeatherResponse getWeather() throws IndegoAuthenticationException, IndegoException {
172 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/weather", LocationWeatherResponse.class);
176 * Queries the predictive adjustment.
178 * @return the predictive adjustment
179 * @throws IndegoAuthenticationException if request was rejected as unauthorized
180 * @throws IndegoException if any communication or parsing error occurred
182 public int getPredictiveAdjustment() throws IndegoAuthenticationException, IndegoException {
183 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/useradjustment",
184 PredictiveAdjustment.class).adjustment;
188 * Sets the predictive adjustment.
190 * @param adjust the predictive adjustment
191 * @throws IndegoAuthenticationException if request was rejected as unauthorized
192 * @throws IndegoException if any communication or parsing error occurred
194 public void setPredictiveAdjustment(final int adjust) throws IndegoAuthenticationException, IndegoException {
195 final PredictiveAdjustment adjustment = new PredictiveAdjustment();
196 adjustment.adjustment = adjust;
197 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/useradjustment", adjustment);
201 * Queries predictive moving.
203 * @return predictive moving
204 * @throws IndegoAuthenticationException if request was rejected as unauthorized
205 * @throws IndegoException if any communication or parsing error occurred
207 public boolean getPredictiveMoving() throws IndegoAuthenticationException, IndegoException {
208 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive", PredictiveStatus.class).enabled;
212 * Sets predictive moving.
215 * @throws IndegoAuthenticationException if request was rejected as unauthorized
216 * @throws IndegoException if any communication or parsing error occurred
218 public void setPredictiveMoving(final boolean enable) throws IndegoAuthenticationException, IndegoException {
219 final PredictiveStatus status = new PredictiveStatus();
220 status.enabled = enable;
221 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive", status);
225 * Queries predictive last cutting as {@link Instant}.
227 * @return predictive last cutting
228 * @throws IndegoAuthenticationException if request was rejected as unauthorized
229 * @throws IndegoException if any communication or parsing error occurred
231 public @Nullable Instant getPredictiveLastCutting() throws IndegoAuthenticationException, IndegoException {
233 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/lastcutting",
234 PredictiveLastCuttingResponse.class).getLastCutting();
235 } catch (IndegoInvalidResponseException e) {
236 if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
244 * Queries predictive next cutting as {@link Instant}.
246 * @return predictive next cutting
247 * @throws IndegoAuthenticationException if request was rejected as unauthorized
248 * @throws IndegoException if any communication or parsing error occurred
250 public @Nullable Instant getPredictiveNextCutting() throws IndegoAuthenticationException, IndegoException {
252 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/nextcutting",
253 PredictiveNextCuttingResponse.class).getNextCutting();
254 } catch (IndegoInvalidResponseException e) {
255 if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
263 * Queries predictive exclusion time.
265 * @return predictive exclusion time DTO
266 * @throws IndegoAuthenticationException if request was rejected as unauthorized
267 * @throws IndegoException if any communication or parsing error occurred
269 public DeviceCalendarResponse getPredictiveExclusionTime() throws IndegoAuthenticationException, IndegoException {
270 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/calendar", DeviceCalendarResponse.class);
274 * Sets predictive exclusion time.
276 * @param calendar calendar DTO
277 * @throws IndegoAuthenticationException if request was rejected as unauthorized
278 * @throws IndegoException if any communication or parsing error occurred
280 public void setPredictiveExclusionTime(final DeviceCalendarResponse calendar)
281 throws IndegoAuthenticationException, IndegoException {
282 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/calendar", calendar);
286 * Request map position updates for the next ({@link count} * {@link interval}) number of seconds.
288 * @param count number of updates
289 * @param interval number of seconds between updates
290 * @throws IndegoAuthenticationException if request was rejected as unauthorized
291 * @throws IndegoException if any communication or parsing error occurred
293 public void requestPosition(int count, int interval) throws IndegoAuthenticationException, IndegoException {
294 postRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/requestPosition?count=" + count + "&interval=" + interval);