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;
16 import java.time.Duration;
17 import java.time.Instant;
18 import java.util.Base64;
19 import java.util.concurrent.ExecutionException;
20 import java.util.concurrent.TimeoutException;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.eclipse.jetty.client.HttpClient;
25 import org.eclipse.jetty.client.HttpResponseException;
26 import org.eclipse.jetty.client.api.ContentResponse;
27 import org.eclipse.jetty.client.api.Request;
28 import org.eclipse.jetty.client.api.Response;
29 import org.eclipse.jetty.client.util.StringContentProvider;
30 import org.eclipse.jetty.http.HttpHeader;
31 import org.eclipse.jetty.http.HttpMethod;
32 import org.eclipse.jetty.http.HttpStatus;
33 import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
34 import org.openhab.binding.boschindego.internal.dto.PredictiveAdjustment;
35 import org.openhab.binding.boschindego.internal.dto.PredictiveStatus;
36 import org.openhab.binding.boschindego.internal.dto.request.AuthenticationRequest;
37 import org.openhab.binding.boschindego.internal.dto.request.SetStateRequest;
38 import org.openhab.binding.boschindego.internal.dto.response.AuthenticationResponse;
39 import org.openhab.binding.boschindego.internal.dto.response.DeviceCalendarResponse;
40 import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse;
41 import org.openhab.binding.boschindego.internal.dto.response.ErrorResponse;
42 import org.openhab.binding.boschindego.internal.dto.response.LocationWeatherResponse;
43 import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse;
44 import org.openhab.binding.boschindego.internal.dto.response.PredictiveLastCuttingResponse;
45 import org.openhab.binding.boschindego.internal.dto.response.PredictiveNextCuttingResponse;
46 import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
47 import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
48 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
49 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
50 import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
51 import org.openhab.core.library.types.RawType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
55 import com.google.gson.Gson;
56 import com.google.gson.JsonParseException;
59 * Controller for communicating with a Bosch Indego device through Bosch services.
60 * This class provides methods for retrieving state information as well as controlling
63 * The implementation is based on zazaz-de/iot-device-bosch-indego-controller, but
64 * rewritten from scratch to use Jetty HTTP client for HTTP communication and GSON for
65 * JSON parsing. Thanks to Oliver Schünemann for providing the original implementation.
67 * @author Jacob Laursen - Initial contribution
70 public class IndegoController {
72 private static final String BASE_URL = "https://api.indego.iot.bosch-si.com/api/v1/";
73 private static final URI BASE_URI = URI.create(BASE_URL);
74 private static final String SERIAL_NUMBER_SUBPATH = "alms/";
75 private static final String SSO_COOKIE_NAME = "BOSCH_INDEGO_SSO";
76 private static final String CONTEXT_HEADER_NAME = "x-im-context-id";
77 private static final String CONTENT_TYPE_HEADER = "application/json";
79 private final Logger logger = LoggerFactory.getLogger(IndegoController.class);
80 private final String basicAuthenticationHeader;
81 private final Gson gson = new Gson();
82 private final HttpClient httpClient;
84 private IndegoSession session = new IndegoSession();
87 * Initialize the controller instance.
89 * @param username the username for authenticating
90 * @param password the password
92 public IndegoController(HttpClient httpClient, String username, String password) {
93 this.httpClient = httpClient;
94 basicAuthenticationHeader = "Basic "
95 + Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
99 * Authenticate with server and store session context and serial number.
101 * @throws IndegoAuthenticationException if request was rejected as unauthorized
102 * @throws IndegoException if any communication or parsing error occurred
104 private void authenticate() throws IndegoAuthenticationException, IndegoException {
107 Request request = httpClient.newRequest(BASE_URL + "authenticate").method(HttpMethod.POST)
108 .header(HttpHeader.AUTHORIZATION, basicAuthenticationHeader);
110 AuthenticationRequest authRequest = new AuthenticationRequest();
111 authRequest.device = "";
112 authRequest.osType = "Android";
113 authRequest.osVersion = "4.0";
114 authRequest.deviceManufacturer = "unknown";
115 authRequest.deviceType = "unknown";
116 String json = gson.toJson(authRequest);
117 request.content(new StringContentProvider(json));
118 request.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_HEADER);
120 if (logger.isTraceEnabled()) {
121 logger.trace("POST request for {}", BASE_URL + "authenticate");
124 ContentResponse response = sendRequest(request);
125 status = response.getStatus();
126 if (status == HttpStatus.UNAUTHORIZED_401) {
127 throw new IndegoAuthenticationException("Authentication was rejected");
129 if (!HttpStatus.isSuccess(status)) {
130 throw new IndegoAuthenticationException("The request failed with HTTP error: " + status);
133 String jsonResponse = response.getContentAsString();
134 if (jsonResponse.isEmpty()) {
135 throw new IndegoInvalidResponseException("No content returned", status);
137 logger.trace("JSON response: '{}'", jsonResponse);
139 AuthenticationResponse authenticationResponse = gson.fromJson(jsonResponse, AuthenticationResponse.class);
140 if (authenticationResponse == null) {
141 throw new IndegoInvalidResponseException("Response could not be parsed as AuthenticationResponse",
144 session = new IndegoSession(authenticationResponse.contextId, authenticationResponse.serialNumber,
145 getContextExpirationTimeFromCookie());
146 logger.debug("Initialized session {}", session);
147 } catch (JsonParseException e) {
148 throw new IndegoInvalidResponseException("Error parsing AuthenticationResponse", e, status);
149 } catch (InterruptedException e) {
150 Thread.currentThread().interrupt();
151 throw new IndegoException(e);
152 } catch (TimeoutException | ExecutionException e) {
153 throw new IndegoException(e);
158 * Get context expiration time as a calculated {@link Instant} relative to now.
159 * The information is obtained from max age in the Bosch Indego SSO cookie.
160 * Please note that this cookie is only sent initially when authenticating, so
161 * the value will not be subject to any updates.
163 * @return expiration time as {@link Instant} or {@link Instant#MIN} if not present
165 private Instant getContextExpirationTimeFromCookie() {
166 return httpClient.getCookieStore().get(BASE_URI).stream().filter(c -> SSO_COOKIE_NAME.equals(c.getName()))
167 .findFirst().map(c -> {
168 return Instant.now().plusSeconds(c.getMaxAge());
175 * Deauthenticate session. This method should be called as part of cleanup to reduce
176 * lingering sessions. This can potentially avoid killed sessions in situation with
177 * multiple clients (e.g. openHAB and mobile app) if restrictions on concurrent
178 * number of sessions would be put on the service.
180 * @throws IndegoException if any communication or parsing error occurred
182 public void deauthenticate() throws IndegoException {
183 if (session.isValid()) {
184 deleteRequest("authenticate");
185 session.invalidate();
190 * Wraps {@link #getRequest(String, Class)} into an authenticated session.
192 * @param path the relative path to which the request should be sent
193 * @param dtoClass the DTO class to which the JSON result should be deserialized
194 * @return the deserialized DTO from the JSON response
195 * @throws IndegoAuthenticationException if request was rejected as unauthorized
196 * @throws IndegoTimeoutException if device cannot be reached (gateway timeout error)
197 * @throws IndegoException if any communication or parsing error occurred
199 private <T> T getRequestWithAuthentication(String path, Class<? extends T> dtoClass)
200 throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
201 if (!session.isValid()) {
205 logger.debug("Session {} valid, skipping authentication", session);
206 return getRequest(path, dtoClass);
207 } catch (IndegoAuthenticationException e) {
208 if (logger.isTraceEnabled()) {
209 logger.trace("Context rejected", e);
211 logger.debug("Context rejected: {}", e.getMessage());
213 session.invalidate();
215 return getRequest(path, dtoClass);
220 * Sends a GET request to the server and returns the deserialized JSON response.
222 * @param path the relative path to which the request should be sent
223 * @param dtoClass the DTO class to which the JSON result should be deserialized
224 * @return the deserialized DTO from the JSON response
225 * @throws IndegoAuthenticationException if request was rejected as unauthorized
226 * @throws IndegoTimeoutException if device cannot be reached (gateway timeout error)
227 * @throws IndegoException if any communication or parsing error occurred
229 private <T> T getRequest(String path, Class<? extends T> dtoClass)
230 throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
233 Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET).header(CONTEXT_HEADER_NAME,
234 session.getContextId());
235 if (logger.isTraceEnabled()) {
236 logger.trace("GET request for {}", BASE_URL + path);
238 ContentResponse response = sendRequest(request);
239 status = response.getStatus();
240 String jsonResponse = response.getContentAsString();
241 if (!jsonResponse.isEmpty()) {
242 logger.trace("JSON response: '{}'", jsonResponse);
244 if (status == HttpStatus.UNAUTHORIZED_401) {
245 // This will currently not happen because "WWW-Authenticate" header is missing; see below.
246 throw new IndegoAuthenticationException("Context rejected");
248 if (status == HttpStatus.GATEWAY_TIMEOUT_504) {
249 throw new IndegoTimeoutException("Gateway timeout");
251 if (!HttpStatus.isSuccess(status)) {
252 throw new IndegoException("The request failed with error: " + status);
254 if (jsonResponse.isEmpty()) {
255 throw new IndegoInvalidResponseException("No content returned", status);
259 T result = gson.fromJson(jsonResponse, dtoClass);
260 if (result == null) {
261 throw new IndegoInvalidResponseException("Parsed response is null", status);
264 } catch (JsonParseException e) {
265 throw new IndegoInvalidResponseException("Error parsing response", e, status);
266 } catch (InterruptedException e) {
267 Thread.currentThread().interrupt();
268 throw new IndegoException(e);
269 } catch (TimeoutException e) {
270 throw new IndegoException(e);
271 } catch (ExecutionException e) {
272 Throwable cause = e.getCause();
273 if (cause != null && cause instanceof HttpResponseException) {
274 Response response = ((HttpResponseException) cause).getResponse();
275 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
277 * When contextId is not valid, the service will respond with HTTP code 401 without
278 * any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
279 * HttpResponseException. We need to handle this in order to attempt
282 throw new IndegoAuthenticationException("Context rejected", e);
285 throw new IndegoException(e);
290 * Wraps {@link #getRawRequest(String)} into an authenticated session.
292 * @param path the relative path to which the request should be sent
293 * @return the raw data from the response
294 * @throws IndegoAuthenticationException if request was rejected as unauthorized
295 * @throws IndegoException if any communication or parsing error occurred
297 private RawType getRawRequestWithAuthentication(String path) throws IndegoAuthenticationException, IndegoException {
298 if (!session.isValid()) {
302 logger.debug("Session {} valid, skipping authentication", session);
303 return getRawRequest(path);
304 } catch (IndegoAuthenticationException e) {
305 if (logger.isTraceEnabled()) {
306 logger.trace("Context rejected", e);
308 logger.debug("Context rejected: {}", e.getMessage());
310 session.invalidate();
312 return getRawRequest(path);
317 * Sends a GET request to the server and returns the raw response.
319 * @param path the relative path to which the request should be sent
320 * @return the raw data from the response
321 * @throws IndegoAuthenticationException if request was rejected as unauthorized
322 * @throws IndegoException if any communication or parsing error occurred
324 private RawType getRawRequest(String path) throws IndegoAuthenticationException, IndegoException {
327 Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET).header(CONTEXT_HEADER_NAME,
328 session.getContextId());
329 if (logger.isTraceEnabled()) {
330 logger.trace("GET request for {}", BASE_URL + path);
332 ContentResponse response = sendRequest(request);
333 status = response.getStatus();
334 if (status == HttpStatus.UNAUTHORIZED_401) {
335 // This will currently not happen because "WWW-Authenticate" header is missing; see below.
336 throw new IndegoAuthenticationException("Context rejected");
338 if (!HttpStatus.isSuccess(status)) {
339 throw new IndegoException("The request failed with error: " + status);
341 byte[] data = response.getContent();
343 throw new IndegoInvalidResponseException("No data returned", status);
345 String contentType = response.getMediaType();
346 if (contentType == null || contentType.isEmpty()) {
347 throw new IndegoInvalidResponseException("No content-type returned", status);
349 logger.debug("Media download response: type {}, length {}", contentType, data.length);
351 return new RawType(data, contentType);
352 } catch (JsonParseException e) {
353 throw new IndegoInvalidResponseException("Error parsing response", e, status);
354 } catch (InterruptedException e) {
355 Thread.currentThread().interrupt();
356 throw new IndegoException(e);
357 } catch (TimeoutException e) {
358 throw new IndegoException(e);
359 } catch (ExecutionException e) {
360 Throwable cause = e.getCause();
361 if (cause != null && cause instanceof HttpResponseException) {
362 Response response = ((HttpResponseException) cause).getResponse();
363 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
365 * When contextId is not valid, the service will respond with HTTP code 401 without
366 * any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
367 * HttpResponseException. We need to handle this in order to attempt
370 throw new IndegoAuthenticationException("Context rejected", e);
373 throw new IndegoException(e);
378 * Wraps {@link #putPostRequest(HttpMethod, String, Object)} into an authenticated session.
380 * @param path the relative path to which the request should be sent
381 * @param requestDto the DTO which should be sent to the server as JSON
382 * @throws IndegoAuthenticationException if request was rejected as unauthorized
383 * @throws IndegoException if any communication or parsing error occurred
385 private void putRequestWithAuthentication(String path, Object requestDto)
386 throws IndegoAuthenticationException, IndegoException {
387 if (!session.isValid()) {
391 logger.debug("Session {} valid, skipping authentication", session);
392 putPostRequest(HttpMethod.PUT, path, requestDto);
393 } catch (IndegoAuthenticationException e) {
394 if (logger.isTraceEnabled()) {
395 logger.trace("Context rejected", e);
397 logger.debug("Context rejected: {}", e.getMessage());
399 session.invalidate();
401 putPostRequest(HttpMethod.PUT, path, requestDto);
406 * Wraps {@link #putPostRequest(HttpMethod, String, Object)} into an authenticated session.
408 * @param path the relative path to which the request should be sent
409 * @throws IndegoAuthenticationException if request was rejected as unauthorized
410 * @throws IndegoException if any communication or parsing error occurred
412 private void postRequestWithAuthentication(String path) throws IndegoAuthenticationException, IndegoException {
413 if (!session.isValid()) {
417 logger.debug("Session {} valid, skipping authentication", session);
418 putPostRequest(HttpMethod.POST, path, null);
419 } catch (IndegoAuthenticationException e) {
420 if (logger.isTraceEnabled()) {
421 logger.trace("Context rejected", e);
423 logger.debug("Context rejected: {}", e.getMessage());
425 session.invalidate();
427 putPostRequest(HttpMethod.POST, path, null);
432 * Sends a PUT/POST request to the server.
434 * @param method the type of request ({@link HttpMethod.PUT} or {@link HttpMethod.POST})
435 * @param path the relative path to which the request should be sent
436 * @param requestDto the DTO which should be sent to the server as JSON
437 * @throws IndegoAuthenticationException if request was rejected as unauthorized
438 * @throws IndegoException if any communication or parsing error occurred
440 private void putPostRequest(HttpMethod method, String path, @Nullable Object requestDto)
441 throws IndegoAuthenticationException, IndegoException {
443 Request request = httpClient.newRequest(BASE_URL + path).method(method)
444 .header(CONTEXT_HEADER_NAME, session.getContextId())
445 .header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_HEADER);
446 if (requestDto != null) {
447 String payload = gson.toJson(requestDto);
448 request.content(new StringContentProvider(payload));
449 if (logger.isTraceEnabled()) {
450 logger.trace("{} request for {} with payload '{}'", method, BASE_URL + path, payload);
453 logger.trace("{} request for {} with no payload", method, BASE_URL + path);
455 ContentResponse response = sendRequest(request);
456 String jsonResponse = response.getContentAsString();
457 if (!jsonResponse.isEmpty()) {
458 logger.trace("JSON response: '{}'", jsonResponse);
460 int status = response.getStatus();
461 if (status == HttpStatus.UNAUTHORIZED_401) {
462 // This will currently not happen because "WWW-Authenticate" header is missing; see below.
463 throw new IndegoAuthenticationException("Context rejected");
465 if (status == HttpStatus.INTERNAL_SERVER_ERROR_500) {
467 ErrorResponse result = gson.fromJson(jsonResponse, ErrorResponse.class);
468 if (result != null) {
469 throw new IndegoInvalidCommandException("The request failed with HTTP error: " + status,
472 } catch (JsonParseException e) {
473 // Ignore missing error code, next line will throw.
475 throw new IndegoInvalidCommandException("The request failed with HTTP error: " + status);
477 if (!HttpStatus.isSuccess(status)) {
478 throw new IndegoException("The request failed with error: " + status);
480 } catch (JsonParseException e) {
481 throw new IndegoException("Error serializing request", e);
482 } catch (InterruptedException e) {
483 Thread.currentThread().interrupt();
484 throw new IndegoException(e);
485 } catch (TimeoutException e) {
486 throw new IndegoException(e);
487 } catch (ExecutionException e) {
488 Throwable cause = e.getCause();
489 if (cause != null && cause instanceof HttpResponseException) {
490 Response response = ((HttpResponseException) cause).getResponse();
491 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
493 * When contextId is not valid, the service will respond with HTTP code 401 without
494 * any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
495 * HttpResponseException. We need to handle this in order to attempt
498 throw new IndegoAuthenticationException("Context rejected", e);
501 throw new IndegoException(e);
506 * Sends a DELETE request to the server.
508 * @param path the relative path to which the request should be sent
509 * @throws IndegoException if any communication or parsing error occurred
511 private void deleteRequest(String path) throws IndegoException {
513 Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.DELETE)
514 .header(CONTEXT_HEADER_NAME, session.getContextId());
515 if (logger.isTraceEnabled()) {
516 logger.trace("DELETE request for {}", BASE_URL + path);
518 ContentResponse response = sendRequest(request);
519 int status = response.getStatus();
520 if (!HttpStatus.isSuccess(status)) {
521 throw new IndegoException("The request failed with error: " + status);
523 } catch (InterruptedException e) {
524 Thread.currentThread().interrupt();
525 throw new IndegoException(e);
526 } catch (TimeoutException | ExecutionException e) {
527 throw new IndegoException(e);
532 * Send request. This method exists for the purpose of avoiding multiple calls to
533 * the server at the same time.
535 * @param request the {@link Request} to send
536 * @return a {@link ContentResponse} for this request
537 * @throws InterruptedException if send thread is interrupted
538 * @throws TimeoutException if send times out
539 * @throws ExecutionException if execution fails
541 private synchronized ContentResponse sendRequest(Request request)
542 throws InterruptedException, TimeoutException, ExecutionException {
543 return request.send();
547 * Gets serial number of the associated Indego device
549 * @return the serial number of the device
550 * @throws IndegoAuthenticationException if request was rejected as unauthorized
551 * @throws IndegoException if any communication or parsing error occurred
553 public synchronized String getSerialNumber() throws IndegoAuthenticationException, IndegoException {
554 if (!session.isInitialized()) {
555 logger.debug("Session not yet initialized when serial number was requested; authenticating...");
558 return session.getSerialNumber();
562 * Queries the device state from the server.
564 * @return the device state
565 * @throws IndegoAuthenticationException if request was rejected as unauthorized
566 * @throws IndegoException if any communication or parsing error occurred
568 public DeviceStateResponse getState() throws IndegoAuthenticationException, IndegoException {
569 return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/state",
570 DeviceStateResponse.class);
574 * Queries the device state from the server. This overload will return when the state
575 * has changed, or the timeout has been reached.
577 * @param timeout Maximum time to wait for response
578 * @return the device state
579 * @throws IndegoAuthenticationException if request was rejected as unauthorized
580 * @throws IndegoException if any communication or parsing error occurred
582 public DeviceStateResponse getState(Duration timeout) throws IndegoAuthenticationException, IndegoException {
583 return getRequestWithAuthentication(
584 SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/state?longpoll=true&timeout=" + timeout.getSeconds(),
585 DeviceStateResponse.class);
589 * Queries the device operating data from the server.
590 * Server will request this directly from the device, so operation might be slow.
592 * @return the device state
593 * @throws IndegoAuthenticationException if request was rejected as unauthorized
594 * @throws IndegoTimeoutException if device cannot be reached (gateway timeout error)
595 * @throws IndegoException if any communication or parsing error occurred
597 public OperatingDataResponse getOperatingData()
598 throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
599 return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/operatingData",
600 OperatingDataResponse.class);
604 * Queries the map generated by the device from the server.
606 * @return the garden map
607 * @throws IndegoAuthenticationException if request was rejected as unauthorized
608 * @throws IndegoException if any communication or parsing error occurred
610 public RawType getMap() throws IndegoAuthenticationException, IndegoException {
611 return getRawRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/map");
615 * Queries the calendar.
617 * @return the calendar
618 * @throws IndegoAuthenticationException if request was rejected as unauthorized
619 * @throws IndegoException if any communication or parsing error occurred
621 public DeviceCalendarResponse getCalendar() throws IndegoAuthenticationException, IndegoException {
622 DeviceCalendarResponse calendar = getRequestWithAuthentication(
623 SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/calendar", DeviceCalendarResponse.class);
628 * Sends a command to the Indego device.
630 * @param command the control command to send to the device
631 * @throws IndegoAuthenticationException if request was rejected as unauthorized
632 * @throws IndegoInvalidCommandException if the command was not processed correctly
633 * @throws IndegoException if any communication or parsing error occurred
635 public void sendCommand(DeviceCommand command)
636 throws IndegoAuthenticationException, IndegoInvalidCommandException, IndegoException {
637 SetStateRequest request = new SetStateRequest();
638 request.state = command.getActionCode();
639 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/state", request);
643 * Queries the predictive weather forecast.
645 * @return the weather forecast DTO
646 * @throws IndegoAuthenticationException if request was rejected as unauthorized
647 * @throws IndegoException if any communication or parsing error occurred
649 public LocationWeatherResponse getWeather() throws IndegoAuthenticationException, IndegoException {
650 return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/weather",
651 LocationWeatherResponse.class);
655 * Queries the predictive adjustment.
657 * @return the predictive adjustment
658 * @throws IndegoAuthenticationException if request was rejected as unauthorized
659 * @throws IndegoException if any communication or parsing error occurred
661 public int getPredictiveAdjustment() throws IndegoAuthenticationException, IndegoException {
662 return getRequestWithAuthentication(
663 SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/useradjustment",
664 PredictiveAdjustment.class).adjustment;
668 * Sets the predictive adjustment.
670 * @param adjust the predictive adjustment
671 * @throws IndegoAuthenticationException if request was rejected as unauthorized
672 * @throws IndegoException if any communication or parsing error occurred
674 public void setPredictiveAdjustment(final int adjust) throws IndegoAuthenticationException, IndegoException {
675 final PredictiveAdjustment adjustment = new PredictiveAdjustment();
676 adjustment.adjustment = adjust;
677 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/useradjustment",
682 * Queries predictive moving.
684 * @return predictive moving
685 * @throws IndegoAuthenticationException if request was rejected as unauthorized
686 * @throws IndegoException if any communication or parsing error occurred
688 public boolean getPredictiveMoving() throws IndegoAuthenticationException, IndegoException {
689 return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive",
690 PredictiveStatus.class).enabled;
694 * Sets predictive moving.
697 * @throws IndegoAuthenticationException if request was rejected as unauthorized
698 * @throws IndegoException if any communication or parsing error occurred
700 public void setPredictiveMoving(final boolean enable) throws IndegoAuthenticationException, IndegoException {
701 final PredictiveStatus status = new PredictiveStatus();
702 status.enabled = enable;
703 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive", status);
707 * Queries predictive last cutting as {@link Instant}.
709 * @return predictive last cutting
710 * @throws IndegoAuthenticationException if request was rejected as unauthorized
711 * @throws IndegoException if any communication or parsing error occurred
713 public @Nullable Instant getPredictiveLastCutting() throws IndegoAuthenticationException, IndegoException {
715 return getRequestWithAuthentication(
716 SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/lastcutting",
717 PredictiveLastCuttingResponse.class).getLastCutting();
718 } catch (IndegoInvalidResponseException e) {
719 if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
727 * Queries predictive next cutting as {@link Instant}.
729 * @return predictive next cutting
730 * @throws IndegoAuthenticationException if request was rejected as unauthorized
731 * @throws IndegoException if any communication or parsing error occurred
733 public @Nullable Instant getPredictiveNextCutting() throws IndegoAuthenticationException, IndegoException {
735 return getRequestWithAuthentication(
736 SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/nextcutting",
737 PredictiveNextCuttingResponse.class).getNextCutting();
738 } catch (IndegoInvalidResponseException e) {
739 if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
747 * Queries predictive exclusion time.
749 * @return predictive exclusion time DTO
750 * @throws IndegoAuthenticationException if request was rejected as unauthorized
751 * @throws IndegoException if any communication or parsing error occurred
753 public DeviceCalendarResponse getPredictiveExclusionTime() throws IndegoAuthenticationException, IndegoException {
754 return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/calendar",
755 DeviceCalendarResponse.class);
759 * Sets predictive exclusion time.
761 * @param calendar calendar DTO
762 * @throws IndegoAuthenticationException if request was rejected as unauthorized
763 * @throws IndegoException if any communication or parsing error occurred
765 public void setPredictiveExclusionTime(final DeviceCalendarResponse calendar)
766 throws IndegoAuthenticationException, IndegoException {
767 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/calendar", calendar);
771 * Request map position updates for the next ({@link count} * {@link interval}) number of seconds.
773 * @param count Number of updates
774 * @param interval Number of seconds between updates
775 * @throws IndegoAuthenticationException if request was rejected as unauthorized
776 * @throws IndegoException if any communication or parsing error occurred
778 public void requestPosition(int count, int interval) throws IndegoAuthenticationException, IndegoException {
779 postRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/requestPosition?count=" + count
780 + "&interval=" + interval);