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.DeviceStateResponse;
28 import org.openhab.binding.boschindego.internal.dto.response.LocationWeatherResponse;
29 import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse;
30 import org.openhab.binding.boschindego.internal.dto.response.PredictiveLastCuttingResponse;
31 import org.openhab.binding.boschindego.internal.dto.response.PredictiveNextCuttingResponse;
32 import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
33 import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
34 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
35 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
36 import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
37 import org.openhab.core.auth.client.oauth2.OAuthClientService;
38 import org.openhab.core.library.types.RawType;
41 * Controller for communicating with a Bosch Indego device through Bosch services.
42 * This class provides methods for retrieving state information as well as controlling
45 * The implementation is based on zazaz-de's iot-device-bosch-indego-controller, but
46 * rewritten from scratch to use Jetty HTTP client for HTTP communication and GSON for
47 * JSON parsing. Thanks to Oliver Schünemann for providing the original implementation.
50 * "https://github.com/zazaz-de/iot-device-bosch-indego-controller">zazaz-de/iot-device-bosch-indego-controller</a>
52 * @author Jacob Laursen - Initial contribution
55 public class IndegoDeviceController extends IndegoController {
57 private String serialNumber;
60 * Initialize the controller instance.
62 * @param httpClient the HttpClient for communicating with the service
63 * @param oAuthClientService the OAuthClientService for authorization
64 * @param serialNumber the serial number of the device instance
66 public IndegoDeviceController(HttpClient httpClient, OAuthClientService oAuthClientService, String serialNumber) {
67 super(httpClient, oAuthClientService);
68 if (serialNumber.isBlank()) {
69 throw new IllegalArgumentException("Serial number must be provided");
71 this.serialNumber = serialNumber;
75 * Queries the device state from the server.
77 * @return the device state
78 * @throws IndegoAuthenticationException if request was rejected as unauthorized
79 * @throws IndegoException if any communication or parsing error occurred
81 public DeviceStateResponse getState() throws IndegoAuthenticationException, IndegoException {
82 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/state", DeviceStateResponse.class);
86 * Queries the device state from the server. This overload will return when the state
87 * has changed, or the timeout has been reached.
89 * @param timeout maximum time to wait for response
90 * @return the device state
91 * @throws IndegoAuthenticationException if request was rejected as unauthorized
92 * @throws IndegoException if any communication or parsing error occurred
94 public DeviceStateResponse getState(Duration timeout) throws IndegoAuthenticationException, IndegoException {
95 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/state?longpoll=true&timeout=" + timeout.getSeconds(),
96 DeviceStateResponse.class);
100 * Queries the device operating data from the server.
101 * Server will request this directly from the device, so operation might be slow.
103 * @return the device state
104 * @throws IndegoAuthenticationException if request was rejected as unauthorized
105 * @throws IndegoTimeoutException if device cannot be reached (gateway timeout error)
106 * @throws IndegoException if any communication or parsing error occurred
108 public OperatingDataResponse getOperatingData()
109 throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
110 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/operatingData", OperatingDataResponse.class);
114 * Queries the map generated by the device from the server.
116 * @return the garden map
117 * @throws IndegoAuthenticationException if request was rejected as unauthorized
118 * @throws IndegoException if any communication or parsing error occurred
120 public RawType getMap() throws IndegoAuthenticationException, IndegoException {
121 return getRawRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/map");
125 * Queries the calendar.
127 * @return the calendar
128 * @throws IndegoAuthenticationException if request was rejected as unauthorized
129 * @throws IndegoException if any communication or parsing error occurred
131 public DeviceCalendarResponse getCalendar() throws IndegoAuthenticationException, IndegoException {
132 DeviceCalendarResponse calendar = getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/calendar",
133 DeviceCalendarResponse.class);
138 * Sends a command to the Indego device.
140 * @param command the control command to send to the device
141 * @throws IndegoAuthenticationException if request was rejected as unauthorized
142 * @throws IndegoInvalidCommandException if the command was not processed correctly
143 * @throws IndegoException if any communication or parsing error occurred
145 public void sendCommand(DeviceCommand command)
146 throws IndegoAuthenticationException, IndegoInvalidCommandException, IndegoException {
147 SetStateRequest request = new SetStateRequest();
148 request.state = command.getActionCode();
149 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/state", request);
153 * Queries the predictive weather forecast.
155 * @return the weather forecast DTO
156 * @throws IndegoAuthenticationException if request was rejected as unauthorized
157 * @throws IndegoException if any communication or parsing error occurred
159 public LocationWeatherResponse getWeather() throws IndegoAuthenticationException, IndegoException {
160 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/weather", LocationWeatherResponse.class);
164 * Queries the predictive adjustment.
166 * @return the predictive adjustment
167 * @throws IndegoAuthenticationException if request was rejected as unauthorized
168 * @throws IndegoException if any communication or parsing error occurred
170 public int getPredictiveAdjustment() throws IndegoAuthenticationException, IndegoException {
171 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/useradjustment",
172 PredictiveAdjustment.class).adjustment;
176 * Sets the predictive adjustment.
178 * @param adjust the predictive adjustment
179 * @throws IndegoAuthenticationException if request was rejected as unauthorized
180 * @throws IndegoException if any communication or parsing error occurred
182 public void setPredictiveAdjustment(final int adjust) throws IndegoAuthenticationException, IndegoException {
183 final PredictiveAdjustment adjustment = new PredictiveAdjustment();
184 adjustment.adjustment = adjust;
185 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/useradjustment", adjustment);
189 * Queries predictive moving.
191 * @return predictive moving
192 * @throws IndegoAuthenticationException if request was rejected as unauthorized
193 * @throws IndegoException if any communication or parsing error occurred
195 public boolean getPredictiveMoving() throws IndegoAuthenticationException, IndegoException {
196 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive", PredictiveStatus.class).enabled;
200 * Sets predictive moving.
203 * @throws IndegoAuthenticationException if request was rejected as unauthorized
204 * @throws IndegoException if any communication or parsing error occurred
206 public void setPredictiveMoving(final boolean enable) throws IndegoAuthenticationException, IndegoException {
207 final PredictiveStatus status = new PredictiveStatus();
208 status.enabled = enable;
209 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive", status);
213 * Queries predictive last cutting as {@link Instant}.
215 * @return predictive last cutting
216 * @throws IndegoAuthenticationException if request was rejected as unauthorized
217 * @throws IndegoException if any communication or parsing error occurred
219 public @Nullable Instant getPredictiveLastCutting() throws IndegoAuthenticationException, IndegoException {
221 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/lastcutting",
222 PredictiveLastCuttingResponse.class).getLastCutting();
223 } catch (IndegoInvalidResponseException e) {
224 if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
232 * Queries predictive next cutting as {@link Instant}.
234 * @return predictive next cutting
235 * @throws IndegoAuthenticationException if request was rejected as unauthorized
236 * @throws IndegoException if any communication or parsing error occurred
238 public @Nullable Instant getPredictiveNextCutting() throws IndegoAuthenticationException, IndegoException {
240 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/nextcutting",
241 PredictiveNextCuttingResponse.class).getNextCutting();
242 } catch (IndegoInvalidResponseException e) {
243 if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
251 * Queries predictive exclusion time.
253 * @return predictive exclusion time DTO
254 * @throws IndegoAuthenticationException if request was rejected as unauthorized
255 * @throws IndegoException if any communication or parsing error occurred
257 public DeviceCalendarResponse getPredictiveExclusionTime() throws IndegoAuthenticationException, IndegoException {
258 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/calendar", DeviceCalendarResponse.class);
262 * Sets predictive exclusion time.
264 * @param calendar calendar DTO
265 * @throws IndegoAuthenticationException if request was rejected as unauthorized
266 * @throws IndegoException if any communication or parsing error occurred
268 public void setPredictiveExclusionTime(final DeviceCalendarResponse calendar)
269 throws IndegoAuthenticationException, IndegoException {
270 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/calendar", calendar);
274 * Request map position updates for the next ({@link count} * {@link interval}) number of seconds.
276 * @param count number of updates
277 * @param interval number of seconds between updates
278 * @throws IndegoAuthenticationException if request was rejected as unauthorized
279 * @throws IndegoException if any communication or parsing error occurred
281 public void requestPosition(int count, int interval) throws IndegoAuthenticationException, IndegoException {
282 postRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/requestPosition?count=" + count + "&interval=" + interval);