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.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 authorizationProvider the AuthorizationProvider for authenticating with the service
64 * @param serialNumber the serial number of the device instance
66 public IndegoDeviceController(HttpClient httpClient, AuthorizationProvider authorizationProvider,
67 String serialNumber) {
68 super(httpClient, authorizationProvider);
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 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/calendar", DeviceCalendarResponse.class);
148 * Sends a command to the Indego device.
150 * @param command the control command to send to the device
151 * @throws IndegoAuthenticationException if request was rejected as unauthorized
152 * @throws IndegoInvalidCommandException if the command was not processed correctly
153 * @throws IndegoException if any communication or parsing error occurred
155 public void sendCommand(DeviceCommand command)
156 throws IndegoAuthenticationException, IndegoInvalidCommandException, IndegoException {
157 SetStateRequest request = new SetStateRequest();
158 request.state = command.getActionCode();
159 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/state", request);
163 * Queries the predictive weather forecast.
165 * @return the weather forecast DTO
166 * @throws IndegoAuthenticationException if request was rejected as unauthorized
167 * @throws IndegoException if any communication or parsing error occurred
169 public LocationWeatherResponse getWeather() throws IndegoAuthenticationException, IndegoException {
170 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/weather", LocationWeatherResponse.class);
174 * Queries the predictive adjustment.
176 * @return the predictive adjustment
177 * @throws IndegoAuthenticationException if request was rejected as unauthorized
178 * @throws IndegoException if any communication or parsing error occurred
180 public int getPredictiveAdjustment() throws IndegoAuthenticationException, IndegoException {
181 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/useradjustment",
182 PredictiveAdjustment.class).adjustment;
186 * Sets the predictive adjustment.
188 * @param adjust the predictive adjustment
189 * @throws IndegoAuthenticationException if request was rejected as unauthorized
190 * @throws IndegoException if any communication or parsing error occurred
192 public void setPredictiveAdjustment(final int adjust) throws IndegoAuthenticationException, IndegoException {
193 final PredictiveAdjustment adjustment = new PredictiveAdjustment();
194 adjustment.adjustment = adjust;
195 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/useradjustment", adjustment);
199 * Queries predictive moving.
201 * @return predictive moving
202 * @throws IndegoAuthenticationException if request was rejected as unauthorized
203 * @throws IndegoException if any communication or parsing error occurred
205 public boolean getPredictiveMoving() throws IndegoAuthenticationException, IndegoException {
206 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive", PredictiveStatus.class).enabled;
210 * Sets predictive moving.
213 * @throws IndegoAuthenticationException if request was rejected as unauthorized
214 * @throws IndegoException if any communication or parsing error occurred
216 public void setPredictiveMoving(final boolean enable) throws IndegoAuthenticationException, IndegoException {
217 final PredictiveStatus status = new PredictiveStatus();
218 status.enabled = enable;
219 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive", status);
223 * Queries predictive last cutting as {@link Instant}.
225 * @return predictive last cutting
226 * @throws IndegoAuthenticationException if request was rejected as unauthorized
227 * @throws IndegoException if any communication or parsing error occurred
229 public @Nullable Instant getPredictiveLastCutting() throws IndegoAuthenticationException, IndegoException {
231 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/lastcutting",
232 PredictiveLastCuttingResponse.class).getLastCutting();
233 } catch (IndegoInvalidResponseException e) {
234 if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
242 * Queries predictive next cutting as {@link Instant}.
244 * @return predictive next cutting
245 * @throws IndegoAuthenticationException if request was rejected as unauthorized
246 * @throws IndegoException if any communication or parsing error occurred
248 public @Nullable Instant getPredictiveNextCutting() throws IndegoAuthenticationException, IndegoException {
250 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/nextcutting",
251 PredictiveNextCuttingResponse.class).getNextCutting();
252 } catch (IndegoInvalidResponseException e) {
253 if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
261 * Queries predictive exclusion time.
263 * @return predictive exclusion time DTO
264 * @throws IndegoAuthenticationException if request was rejected as unauthorized
265 * @throws IndegoException if any communication or parsing error occurred
267 public DeviceCalendarResponse getPredictiveExclusionTime() throws IndegoAuthenticationException, IndegoException {
268 return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/calendar", DeviceCalendarResponse.class);
272 * Sets predictive exclusion time.
274 * @param calendar calendar DTO
275 * @throws IndegoAuthenticationException if request was rejected as unauthorized
276 * @throws IndegoException if any communication or parsing error occurred
278 public void setPredictiveExclusionTime(final DeviceCalendarResponse calendar)
279 throws IndegoAuthenticationException, IndegoException {
280 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + serialNumber + "/predictive/calendar", calendar);
284 * Request map position updates for the next ({@code count} * {@code interval}) number of seconds.
286 * @param count number of updates
287 * @param interval number of seconds between updates
288 * @throws IndegoAuthenticationException if request was rejected as unauthorized
289 * @throws IndegoException if any communication or parsing error occurred
291 public void requestPosition(int count, int interval) throws IndegoAuthenticationException, IndegoException {
292 postRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/requestPosition?count=" + count + "&interval=" + interval);