2 * Copyright (c) 2010-2022 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.LocationWeatherResponse;
42 import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse;
43 import org.openhab.binding.boschindego.internal.dto.response.PredictiveLastCuttingResponse;
44 import org.openhab.binding.boschindego.internal.dto.response.PredictiveNextCuttingResponse;
45 import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
46 import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
47 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
48 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
49 import org.openhab.binding.boschindego.internal.exceptions.IndegoUnreachableException;
50 import org.openhab.core.library.types.RawType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
54 import com.google.gson.Gson;
55 import com.google.gson.JsonParseException;
58 * Controller for communicating with a Bosch Indego device through Bosch services.
59 * This class provides methods for retrieving state information as well as controlling
62 * The implementation is based on zazaz-de/iot-device-bosch-indego-controller, but
63 * rewritten from scratch to use Jetty HTTP client for HTTP communication and GSON for
64 * JSON parsing. Thanks to Oliver Schünemann for providing the original implementation.
66 * @author Jacob Laursen - Initial contribution
69 public class IndegoController {
71 private static final String BASE_URL = "https://api.indego.iot.bosch-si.com/api/v1/";
72 private static final URI BASE_URI = URI.create(BASE_URL);
73 private static final String SERIAL_NUMBER_SUBPATH = "alms/";
74 private static final String SSO_COOKIE_NAME = "BOSCH_INDEGO_SSO";
75 private static final String CONTEXT_HEADER_NAME = "x-im-context-id";
76 private static final String CONTENT_TYPE_HEADER = "application/json";
78 private final Logger logger = LoggerFactory.getLogger(IndegoController.class);
79 private final String basicAuthenticationHeader;
80 private final Gson gson = new Gson();
81 private final HttpClient httpClient;
83 private IndegoSession session = new IndegoSession();
86 * Initialize the controller instance.
88 * @param username the username for authenticating
89 * @param password the password
91 public IndegoController(HttpClient httpClient, String username, String password) {
92 this.httpClient = httpClient;
93 basicAuthenticationHeader = "Basic "
94 + Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
98 * Authenticate with server and store session context and serial number.
100 * @throws IndegoAuthenticationException if request was rejected as unauthorized
101 * @throws IndegoException if any communication or parsing error occurred
103 private void authenticate() throws IndegoAuthenticationException, IndegoException {
106 Request request = httpClient.newRequest(BASE_URL + "authenticate").method(HttpMethod.POST)
107 .header(HttpHeader.AUTHORIZATION, basicAuthenticationHeader);
109 AuthenticationRequest authRequest = new AuthenticationRequest();
110 authRequest.device = "";
111 authRequest.osType = "Android";
112 authRequest.osVersion = "4.0";
113 authRequest.deviceManufacturer = "unknown";
114 authRequest.deviceType = "unknown";
115 String json = gson.toJson(authRequest);
116 request.content(new StringContentProvider(json));
117 request.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_HEADER);
119 if (logger.isTraceEnabled()) {
120 logger.trace("POST request for {}", BASE_URL + "authenticate");
123 ContentResponse response = sendRequest(request);
124 status = response.getStatus();
125 if (status == HttpStatus.UNAUTHORIZED_401) {
126 throw new IndegoAuthenticationException("Authentication was rejected");
128 if (!HttpStatus.isSuccess(status)) {
129 throw new IndegoAuthenticationException("The request failed with HTTP error: " + status);
132 String jsonResponse = response.getContentAsString();
133 if (jsonResponse.isEmpty()) {
134 throw new IndegoInvalidResponseException("No content returned", status);
136 logger.trace("JSON response: '{}'", jsonResponse);
138 AuthenticationResponse authenticationResponse = gson.fromJson(jsonResponse, AuthenticationResponse.class);
139 if (authenticationResponse == null) {
140 throw new IndegoInvalidResponseException("Response could not be parsed as AuthenticationResponse",
143 session = new IndegoSession(authenticationResponse.contextId, authenticationResponse.serialNumber,
144 getContextExpirationTimeFromCookie());
145 logger.debug("Initialized session {}", session);
146 } catch (JsonParseException e) {
147 throw new IndegoInvalidResponseException("Error parsing AuthenticationResponse", e, status);
148 } catch (InterruptedException e) {
149 Thread.currentThread().interrupt();
150 throw new IndegoException(e);
151 } catch (TimeoutException | ExecutionException e) {
152 throw new IndegoException(e);
157 * Get context expiration time as a calculated {@link Instant} relative to now.
158 * The information is obtained from max age in the Bosch Indego SSO cookie.
159 * Please note that this cookie is only sent initially when authenticating, so
160 * the value will not be subject to any updates.
162 * @return expiration time as {@link Instant} or {@link Instant#MIN} if not present
164 private Instant getContextExpirationTimeFromCookie() {
165 return httpClient.getCookieStore().get(BASE_URI).stream().filter(c -> SSO_COOKIE_NAME.equals(c.getName()))
166 .findFirst().map(c -> {
167 return Instant.now().plusSeconds(c.getMaxAge());
174 * Deauthenticate session. This method should be called as part of cleanup to reduce
175 * lingering sessions. This can potentially avoid killed sessions in situation with
176 * multiple clients (e.g. openHAB and mobile app) if restrictions on concurrent
177 * number of sessions would be put on the service.
179 * @throws IndegoException if any communication or parsing error occurred
181 public void deauthenticate() throws IndegoException {
182 if (session.isValid()) {
183 deleteRequest("authenticate");
184 session.invalidate();
189 * Wraps {@link #getRequest(String, Class)} into an authenticated session.
191 * @param path the relative path to which the request should be sent
192 * @param dtoClass the DTO class to which the JSON result should be deserialized
193 * @return the deserialized DTO from the JSON response
194 * @throws IndegoAuthenticationException if request was rejected as unauthorized
195 * @throws IndegoUnreachableException if device cannot be reached (gateway timeout error)
196 * @throws IndegoException if any communication or parsing error occurred
198 private <T> T getRequestWithAuthentication(String path, Class<? extends T> dtoClass)
199 throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
200 if (!session.isValid()) {
204 logger.debug("Session {} valid, skipping authentication", session);
205 return getRequest(path, dtoClass);
206 } catch (IndegoAuthenticationException e) {
207 if (logger.isTraceEnabled()) {
208 logger.trace("Context rejected", e);
210 logger.debug("Context rejected: {}", e.getMessage());
212 session.invalidate();
214 return getRequest(path, dtoClass);
219 * Sends a GET request to the server and returns the deserialized JSON response.
221 * @param path the relative path to which the request should be sent
222 * @param dtoClass the DTO class to which the JSON result should be deserialized
223 * @return the deserialized DTO from the JSON response
224 * @throws IndegoAuthenticationException if request was rejected as unauthorized
225 * @throws IndegoUnreachableException if device cannot be reached (gateway timeout error)
226 * @throws IndegoException if any communication or parsing error occurred
228 private <T> T getRequest(String path, Class<? extends T> dtoClass)
229 throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
232 Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET).header(CONTEXT_HEADER_NAME,
233 session.getContextId());
234 if (logger.isTraceEnabled()) {
235 logger.trace("GET request for {}", BASE_URL + path);
237 ContentResponse response = sendRequest(request);
238 status = response.getStatus();
239 if (status == HttpStatus.UNAUTHORIZED_401) {
240 // This will currently not happen because "WWW-Authenticate" header is missing; see below.
241 throw new IndegoAuthenticationException("Context rejected");
243 if (status == HttpStatus.GATEWAY_TIMEOUT_504) {
244 throw new IndegoUnreachableException("Gateway timeout");
246 if (!HttpStatus.isSuccess(status)) {
247 throw new IndegoException("The request failed with error: " + status);
249 String jsonResponse = response.getContentAsString();
250 if (jsonResponse.isEmpty()) {
251 throw new IndegoInvalidResponseException("No content returned", status);
253 logger.trace("JSON response: '{}'", jsonResponse);
256 T result = gson.fromJson(jsonResponse, dtoClass);
257 if (result == null) {
258 throw new IndegoInvalidResponseException("Parsed response is null", status);
261 } catch (JsonParseException e) {
262 throw new IndegoInvalidResponseException("Error parsing response", e, status);
263 } catch (InterruptedException e) {
264 Thread.currentThread().interrupt();
265 throw new IndegoException(e);
266 } catch (TimeoutException e) {
267 throw new IndegoException(e);
268 } catch (ExecutionException e) {
269 Throwable cause = e.getCause();
270 if (cause != null && cause instanceof HttpResponseException) {
271 Response response = ((HttpResponseException) cause).getResponse();
272 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
274 * When contextId is not valid, the service will respond with HTTP code 401 without
275 * any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
276 * HttpResponseException. We need to handle this in order to attempt
279 throw new IndegoAuthenticationException("Context rejected", e);
282 throw new IndegoException(e);
287 * Wraps {@link #getRawRequest(String)} into an authenticated session.
289 * @param path the relative path to which the request should be sent
290 * @return the raw data from the response
291 * @throws IndegoAuthenticationException if request was rejected as unauthorized
292 * @throws IndegoException if any communication or parsing error occurred
294 private RawType getRawRequestWithAuthentication(String path) throws IndegoAuthenticationException, IndegoException {
295 if (!session.isValid()) {
299 logger.debug("Session {} valid, skipping authentication", session);
300 return getRawRequest(path);
301 } catch (IndegoAuthenticationException e) {
302 if (logger.isTraceEnabled()) {
303 logger.trace("Context rejected", e);
305 logger.debug("Context rejected: {}", e.getMessage());
307 session.invalidate();
309 return getRawRequest(path);
314 * Sends a GET request to the server and returns the raw response.
316 * @param path the relative path to which the request should be sent
317 * @return the raw data from the response
318 * @throws IndegoAuthenticationException if request was rejected as unauthorized
319 * @throws IndegoException if any communication or parsing error occurred
321 private RawType getRawRequest(String path) throws IndegoAuthenticationException, IndegoException {
324 Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET).header(CONTEXT_HEADER_NAME,
325 session.getContextId());
326 if (logger.isTraceEnabled()) {
327 logger.trace("GET request for {}", BASE_URL + path);
329 ContentResponse response = sendRequest(request);
330 status = response.getStatus();
331 if (status == HttpStatus.UNAUTHORIZED_401) {
332 // This will currently not happen because "WWW-Authenticate" header is missing; see below.
333 throw new IndegoAuthenticationException("Context rejected");
335 if (!HttpStatus.isSuccess(status)) {
336 throw new IndegoException("The request failed with error: " + status);
338 byte[] data = response.getContent();
340 throw new IndegoInvalidResponseException("No data returned", status);
342 String contentType = response.getMediaType();
343 if (contentType == null || contentType.isEmpty()) {
344 throw new IndegoInvalidResponseException("No content-type returned", status);
346 logger.debug("Media download response: type {}, length {}", contentType, data.length);
348 return new RawType(data, contentType);
349 } catch (JsonParseException e) {
350 throw new IndegoInvalidResponseException("Error parsing response", e, status);
351 } catch (InterruptedException e) {
352 Thread.currentThread().interrupt();
353 throw new IndegoException(e);
354 } catch (TimeoutException e) {
355 throw new IndegoException(e);
356 } catch (ExecutionException e) {
357 Throwable cause = e.getCause();
358 if (cause != null && cause instanceof HttpResponseException) {
359 Response response = ((HttpResponseException) cause).getResponse();
360 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
362 * When contextId is not valid, the service will respond with HTTP code 401 without
363 * any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
364 * HttpResponseException. We need to handle this in order to attempt
367 throw new IndegoAuthenticationException("Context rejected", e);
370 throw new IndegoException(e);
375 * Wraps {@link #putPostRequest(HttpMethod, String, Object)} into an authenticated session.
377 * @param path the relative path to which the request should be sent
378 * @param requestDto the DTO which should be sent to the server as JSON
379 * @throws IndegoAuthenticationException if request was rejected as unauthorized
380 * @throws IndegoException if any communication or parsing error occurred
382 private void putRequestWithAuthentication(String path, Object requestDto)
383 throws IndegoAuthenticationException, IndegoException {
384 if (!session.isValid()) {
388 logger.debug("Session {} valid, skipping authentication", session);
389 putPostRequest(HttpMethod.PUT, path, requestDto);
390 } catch (IndegoAuthenticationException e) {
391 if (logger.isTraceEnabled()) {
392 logger.trace("Context rejected", e);
394 logger.debug("Context rejected: {}", e.getMessage());
396 session.invalidate();
398 putPostRequest(HttpMethod.PUT, path, requestDto);
403 * Wraps {@link #putPostRequest(HttpMethod, String, Object)} into an authenticated session.
405 * @param path the relative path to which the request should be sent
406 * @throws IndegoAuthenticationException if request was rejected as unauthorized
407 * @throws IndegoException if any communication or parsing error occurred
409 private void postRequestWithAuthentication(String path) throws IndegoAuthenticationException, IndegoException {
410 if (!session.isValid()) {
414 logger.debug("Session {} valid, skipping authentication", session);
415 putPostRequest(HttpMethod.POST, path, null);
416 } catch (IndegoAuthenticationException e) {
417 if (logger.isTraceEnabled()) {
418 logger.trace("Context rejected", e);
420 logger.debug("Context rejected: {}", e.getMessage());
422 session.invalidate();
424 putPostRequest(HttpMethod.POST, path, null);
429 * Sends a PUT/POST request to the server.
431 * @param method the type of request ({@link HttpMethod.PUT} or {@link HttpMethod.POST})
432 * @param path the relative path to which the request should be sent
433 * @param requestDto the DTO which should be sent to the server as JSON
434 * @throws IndegoAuthenticationException if request was rejected as unauthorized
435 * @throws IndegoException if any communication or parsing error occurred
437 private void putPostRequest(HttpMethod method, String path, @Nullable Object requestDto)
438 throws IndegoAuthenticationException, IndegoException {
440 Request request = httpClient.newRequest(BASE_URL + path).method(method)
441 .header(CONTEXT_HEADER_NAME, session.getContextId())
442 .header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_HEADER);
443 if (requestDto != null) {
444 String payload = gson.toJson(requestDto);
445 request.content(new StringContentProvider(payload));
446 if (logger.isTraceEnabled()) {
447 logger.trace("{} request for {} with payload '{}'", method, BASE_URL + path, payload);
450 logger.trace("{} request for {} with no payload", method, BASE_URL + path);
452 ContentResponse response = sendRequest(request);
453 int status = response.getStatus();
454 if (status == HttpStatus.UNAUTHORIZED_401) {
455 // This will currently not happen because "WWW-Authenticate" header is missing; see below.
456 throw new IndegoAuthenticationException("Context rejected");
458 if (status == HttpStatus.INTERNAL_SERVER_ERROR_500) {
459 throw new IndegoInvalidCommandException("The request failed with HTTP error: " + status);
461 if (!HttpStatus.isSuccess(status)) {
462 throw new IndegoException("The request failed with error: " + status);
464 } catch (JsonParseException e) {
465 throw new IndegoException("Error serializing request", e);
466 } catch (InterruptedException e) {
467 Thread.currentThread().interrupt();
468 throw new IndegoException(e);
469 } catch (TimeoutException e) {
470 throw new IndegoException(e);
471 } catch (ExecutionException e) {
472 Throwable cause = e.getCause();
473 if (cause != null && cause instanceof HttpResponseException) {
474 Response response = ((HttpResponseException) cause).getResponse();
475 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
477 * When contextId is not valid, the service will respond with HTTP code 401 without
478 * any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
479 * HttpResponseException. We need to handle this in order to attempt
482 throw new IndegoAuthenticationException("Context rejected", e);
485 throw new IndegoException(e);
490 * Sends a DELETE request to the server.
492 * @param path the relative path to which the request should be sent
493 * @throws IndegoException if any communication or parsing error occurred
495 private void deleteRequest(String path) throws IndegoException {
497 Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.DELETE)
498 .header(CONTEXT_HEADER_NAME, session.getContextId());
499 if (logger.isTraceEnabled()) {
500 logger.trace("DELETE request for {}", BASE_URL + path);
502 ContentResponse response = sendRequest(request);
503 int status = response.getStatus();
504 if (!HttpStatus.isSuccess(status)) {
505 throw new IndegoException("The request failed with error: " + status);
507 } catch (InterruptedException e) {
508 Thread.currentThread().interrupt();
509 throw new IndegoException(e);
510 } catch (TimeoutException | ExecutionException e) {
511 throw new IndegoException(e);
516 * Send request. This method exists for the purpose of avoiding multiple calls to
517 * the server at the same time.
519 * @param request the {@link Request} to send
520 * @return a {@link ContentResponse} for this request
521 * @throws InterruptedException if send thread is interrupted
522 * @throws TimeoutException if send times out
523 * @throws ExecutionException if execution fails
525 private synchronized ContentResponse sendRequest(Request request)
526 throws InterruptedException, TimeoutException, ExecutionException {
527 return request.send();
531 * Gets serial number of the associated Indego device
533 * @return the serial number of the device
534 * @throws IndegoAuthenticationException if request was rejected as unauthorized
535 * @throws IndegoException if any communication or parsing error occurred
537 public synchronized String getSerialNumber() throws IndegoAuthenticationException, IndegoException {
538 if (!session.isInitialized()) {
539 logger.debug("Session not yet initialized when serial number was requested; authenticating...");
542 return session.getSerialNumber();
546 * Queries the device state from the server.
548 * @return the device state
549 * @throws IndegoAuthenticationException if request was rejected as unauthorized
550 * @throws IndegoException if any communication or parsing error occurred
552 public DeviceStateResponse getState() throws IndegoAuthenticationException, IndegoException {
553 return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/state",
554 DeviceStateResponse.class);
558 * Queries the device state from the server. This overload will return when the state
559 * has changed, or the timeout has been reached.
561 * @param timeout Maximum time to wait for response
562 * @return the device state
563 * @throws IndegoAuthenticationException if request was rejected as unauthorized
564 * @throws IndegoException if any communication or parsing error occurred
566 public DeviceStateResponse getState(Duration timeout) throws IndegoAuthenticationException, IndegoException {
567 return getRequestWithAuthentication(
568 SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/state?longpoll=true&timeout=" + timeout.getSeconds(),
569 DeviceStateResponse.class);
573 * Queries the device operating data from the server.
574 * Server will request this directly from the device, so operation might be slow.
576 * @return the device state
577 * @throws IndegoAuthenticationException if request was rejected as unauthorized
578 * @throws IndegoUnreachableException if device cannot be reached (gateway timeout error)
579 * @throws IndegoException if any communication or parsing error occurred
581 public OperatingDataResponse getOperatingData()
582 throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
583 return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/operatingData",
584 OperatingDataResponse.class);
588 * Queries the map generated by the device from the server.
590 * @return the garden map
591 * @throws IndegoAuthenticationException if request was rejected as unauthorized
592 * @throws IndegoException if any communication or parsing error occurred
594 public RawType getMap() throws IndegoAuthenticationException, IndegoException {
595 return getRawRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/map");
599 * Queries the calendar.
601 * @return the calendar
602 * @throws IndegoAuthenticationException if request was rejected as unauthorized
603 * @throws IndegoException if any communication or parsing error occurred
605 public DeviceCalendarResponse getCalendar() throws IndegoAuthenticationException, IndegoException {
606 DeviceCalendarResponse calendar = getRequestWithAuthentication(
607 SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/calendar", DeviceCalendarResponse.class);
612 * Sends a command to the Indego device.
614 * @param command the control command to send to the device
615 * @throws IndegoAuthenticationException if request was rejected as unauthorized
616 * @throws IndegoInvalidCommandException if the command was not processed correctly
617 * @throws IndegoException if any communication or parsing error occurred
619 public void sendCommand(DeviceCommand command)
620 throws IndegoAuthenticationException, IndegoInvalidCommandException, IndegoException {
621 SetStateRequest request = new SetStateRequest();
622 request.state = command.getActionCode();
623 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/state", request);
627 * Queries the predictive weather forecast.
629 * @return the weather forecast DTO
630 * @throws IndegoAuthenticationException if request was rejected as unauthorized
631 * @throws IndegoException if any communication or parsing error occurred
633 public LocationWeatherResponse getWeather() throws IndegoAuthenticationException, IndegoException {
634 return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/weather",
635 LocationWeatherResponse.class);
639 * Queries the predictive adjustment.
641 * @return the predictive adjustment
642 * @throws IndegoAuthenticationException if request was rejected as unauthorized
643 * @throws IndegoException if any communication or parsing error occurred
645 public int getPredictiveAdjustment() throws IndegoAuthenticationException, IndegoException {
646 return getRequestWithAuthentication(
647 SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/useradjustment",
648 PredictiveAdjustment.class).adjustment;
652 * Sets the predictive adjustment.
654 * @param adjust the predictive adjustment
655 * @throws IndegoAuthenticationException if request was rejected as unauthorized
656 * @throws IndegoException if any communication or parsing error occurred
658 public void setPredictiveAdjustment(final int adjust) throws IndegoAuthenticationException, IndegoException {
659 final PredictiveAdjustment adjustment = new PredictiveAdjustment();
660 adjustment.adjustment = adjust;
661 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/useradjustment",
666 * Queries predictive moving.
668 * @return predictive moving
669 * @throws IndegoAuthenticationException if request was rejected as unauthorized
670 * @throws IndegoException if any communication or parsing error occurred
672 public boolean getPredictiveMoving() throws IndegoAuthenticationException, IndegoException {
673 return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive",
674 PredictiveStatus.class).enabled;
678 * Sets predictive moving.
681 * @throws IndegoAuthenticationException if request was rejected as unauthorized
682 * @throws IndegoException if any communication or parsing error occurred
684 public void setPredictiveMoving(final boolean enable) throws IndegoAuthenticationException, IndegoException {
685 final PredictiveStatus status = new PredictiveStatus();
686 status.enabled = enable;
687 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive", status);
691 * Queries predictive last cutting as {@link Instant}.
693 * @return predictive last cutting
694 * @throws IndegoAuthenticationException if request was rejected as unauthorized
695 * @throws IndegoException if any communication or parsing error occurred
697 public @Nullable Instant getPredictiveLastCutting() throws IndegoAuthenticationException, IndegoException {
699 return getRequestWithAuthentication(
700 SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/lastcutting",
701 PredictiveLastCuttingResponse.class).getLastCutting();
702 } catch (IndegoInvalidResponseException e) {
703 if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
711 * Queries predictive next cutting as {@link Instant}.
713 * @return predictive next cutting
714 * @throws IndegoAuthenticationException if request was rejected as unauthorized
715 * @throws IndegoException if any communication or parsing error occurred
717 public @Nullable Instant getPredictiveNextCutting() throws IndegoAuthenticationException, IndegoException {
719 return getRequestWithAuthentication(
720 SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/nextcutting",
721 PredictiveNextCuttingResponse.class).getNextCutting();
722 } catch (IndegoInvalidResponseException e) {
723 if (e.getHttpStatusCode() == HttpStatus.NO_CONTENT_204) {
731 * Queries predictive exclusion time.
733 * @return predictive exclusion time DTO
734 * @throws IndegoAuthenticationException if request was rejected as unauthorized
735 * @throws IndegoException if any communication or parsing error occurred
737 public DeviceCalendarResponse getPredictiveExclusionTime() throws IndegoAuthenticationException, IndegoException {
738 return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/calendar",
739 DeviceCalendarResponse.class);
743 * Sets predictive exclusion time.
745 * @param calendar calendar DTO
746 * @throws IndegoAuthenticationException if request was rejected as unauthorized
747 * @throws IndegoException if any communication or parsing error occurred
749 public void setPredictiveExclusionTime(final DeviceCalendarResponse calendar)
750 throws IndegoAuthenticationException, IndegoException {
751 putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/calendar", calendar);
755 * Request map position updates for the next ({@link count} * {@link interval}) number of seconds.
757 * @param count Number of updates
758 * @param interval Number of seconds between updates
759 * @throws IndegoAuthenticationException if request was rejected as unauthorized
760 * @throws IndegoException if any communication or parsing error occurred
762 public void requestPosition(int count, int interval) throws IndegoAuthenticationException, IndegoException {
763 postRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/requestPosition?count=" + count
764 + "&interval=" + interval);