import org.openhab.binding.boschindego.internal.dto.response.AuthenticationResponse;
import org.openhab.binding.boschindego.internal.dto.response.DeviceCalendarResponse;
import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse;
+import org.openhab.binding.boschindego.internal.dto.response.ErrorResponse;
import org.openhab.binding.boschindego.internal.dto.response.LocationWeatherResponse;
import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse;
import org.openhab.binding.boschindego.internal.dto.response.PredictiveLastCuttingResponse;
import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
-import org.openhab.binding.boschindego.internal.exceptions.IndegoUnreachableException;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
import org.openhab.core.library.types.RawType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
* @param dtoClass the DTO class to which the JSON result should be deserialized
* @return the deserialized DTO from the JSON response
* @throws IndegoAuthenticationException if request was rejected as unauthorized
- * @throws IndegoUnreachableException if device cannot be reached (gateway timeout error)
+ * @throws IndegoTimeoutException if device cannot be reached (gateway timeout error)
* @throws IndegoException if any communication or parsing error occurred
*/
private <T> T getRequestWithAuthentication(String path, Class<? extends T> dtoClass)
- throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
+ throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
if (!session.isValid()) {
authenticate();
}
* @param dtoClass the DTO class to which the JSON result should be deserialized
* @return the deserialized DTO from the JSON response
* @throws IndegoAuthenticationException if request was rejected as unauthorized
- * @throws IndegoUnreachableException if device cannot be reached (gateway timeout error)
+ * @throws IndegoTimeoutException if device cannot be reached (gateway timeout error)
* @throws IndegoException if any communication or parsing error occurred
*/
private <T> T getRequest(String path, Class<? extends T> dtoClass)
- throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
+ throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
int status = 0;
try {
Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET).header(CONTEXT_HEADER_NAME,
}
ContentResponse response = sendRequest(request);
status = response.getStatus();
+ String jsonResponse = response.getContentAsString();
+ if (!jsonResponse.isEmpty()) {
+ logger.trace("JSON response: '{}'", jsonResponse);
+ }
if (status == HttpStatus.UNAUTHORIZED_401) {
// This will currently not happen because "WWW-Authenticate" header is missing; see below.
throw new IndegoAuthenticationException("Context rejected");
}
if (status == HttpStatus.GATEWAY_TIMEOUT_504) {
- throw new IndegoUnreachableException("Gateway timeout");
+ throw new IndegoTimeoutException("Gateway timeout");
}
if (!HttpStatus.isSuccess(status)) {
throw new IndegoException("The request failed with error: " + status);
}
- String jsonResponse = response.getContentAsString();
if (jsonResponse.isEmpty()) {
throw new IndegoInvalidResponseException("No content returned", status);
}
- logger.trace("JSON response: '{}'", jsonResponse);
@Nullable
T result = gson.fromJson(jsonResponse, dtoClass);
logger.trace("{} request for {} with no payload", method, BASE_URL + path);
}
ContentResponse response = sendRequest(request);
+ String jsonResponse = response.getContentAsString();
+ if (!jsonResponse.isEmpty()) {
+ logger.trace("JSON response: '{}'", jsonResponse);
+ }
int status = response.getStatus();
if (status == HttpStatus.UNAUTHORIZED_401) {
// This will currently not happen because "WWW-Authenticate" header is missing; see below.
throw new IndegoAuthenticationException("Context rejected");
}
if (status == HttpStatus.INTERNAL_SERVER_ERROR_500) {
+ try {
+ ErrorResponse result = gson.fromJson(jsonResponse, ErrorResponse.class);
+ if (result != null) {
+ throw new IndegoInvalidCommandException("The request failed with HTTP error: " + status,
+ result.error);
+ }
+ } catch (JsonParseException e) {
+ // Ignore missing error code, next line will throw.
+ }
throw new IndegoInvalidCommandException("The request failed with HTTP error: " + status);
}
if (!HttpStatus.isSuccess(status)) {
*
* @return the device state
* @throws IndegoAuthenticationException if request was rejected as unauthorized
- * @throws IndegoUnreachableException if device cannot be reached (gateway timeout error)
+ * @throws IndegoTimeoutException if device cannot be reached (gateway timeout error)
* @throws IndegoException if any communication or parsing error occurred
*/
public OperatingDataResponse getOperatingData()
- throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
+ throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/operatingData",
OperatingDataResponse.class);
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.dto.response;
+
+/**
+ * Response from PUT request.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class ErrorResponse {
+ public int error;
+}
public class IndegoInvalidCommandException extends IndegoException {
private static final long serialVersionUID = -2946398731437793113L;
+ private int errorCode = 0;
public IndegoInvalidCommandException(String message) {
super(message);
}
+
+ public IndegoInvalidCommandException(String message, int errorCode) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ public boolean hasErrorCode() {
+ return errorCode != 0;
+ }
+
+ public int getErrorCode() {
+ return errorCode;
+ }
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal.exceptions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link IndegoTimeoutException} is thrown on gateway timeout, which
+ * means that Bosch services cannot connect to the device.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class IndegoTimeoutException extends IndegoException {
+
+ private static final long serialVersionUID = -7952585411438042139L;
+
+ public IndegoTimeoutException(String message) {
+ super(message);
+ }
+
+ public IndegoTimeoutException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.boschindego.internal.exceptions;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * {@link IndegoUnreachableException} is thrown on gateway timeout, which
- * means that Bosch services cannot connect to the device.
- *
- * @author Jacob Laursen - Initial contribution
- */
-@NonNullByDefault
-public class IndegoUnreachableException extends IndegoException {
-
- private static final long serialVersionUID = -7952585411438042139L;
-
- public IndegoUnreachableException(String message) {
- super(message);
- }
-
- public IndegoUnreachableException(String message, Throwable cause) {
- super(message, cause);
- }
-}
import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse;
import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
-import org.openhab.binding.boschindego.internal.exceptions.IndegoUnreachableException;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
private static final Duration MAP_REFRESH_INTERVAL = Duration.ofDays(1);
private static final Duration OPERATING_DATA_INACTIVE_REFRESH_INTERVAL = Duration.ofHours(6);
+ private static final Duration OPERATING_DATA_OFFLINE_REFRESH_INTERVAL = Duration.ofMinutes(30);
private static final Duration OPERATING_DATA_ACTIVE_REFRESH_INTERVAL = Duration.ofMinutes(2);
private static final Duration MAP_REFRESH_SESSION_DURATION = Duration.ofMinutes(5);
private static final Duration COMMAND_STATE_REFRESH_TIMEOUT = Duration.ofSeconds(10);
private Instant cachedMapTimestamp = Instant.MIN;
private Instant operatingDataTimestamp = Instant.MIN;
private Instant mapRefreshStartedTimestamp = Instant.MIN;
+ private ThingStatus lastOperatingDataStatus = ThingStatus.UNINITIALIZED;
private int stateInactiveRefreshIntervalSeconds;
private int stateActiveRefreshIntervalSeconds;
private int currentRefreshIntervalSeconds;
} catch (IndegoAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.authentication-failure");
- } catch (IndegoUnreachableException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ } catch (IndegoTimeoutException e) {
+ updateStatus(lastOperatingDataStatus = ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.unreachable");
+ } catch (IndegoInvalidCommandException e) {
+ logger.warn("Invalid command: {}", e.getMessage());
+ if (e.hasErrorCode()) {
+ updateState(ERRORCODE, new DecimalType(e.getErrorCode()));
+ }
} catch (IndegoException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ logger.warn("Command failed: {}", e.getMessage());
}
}
private void handleRefreshCommand(String channelId)
- throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
+ throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
switch (channelId) {
case GARDEN_MAP:
// Force map refresh and fall through to state update.
} catch (IndegoAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.authentication-failure");
- } catch (IndegoUnreachableException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ } catch (IndegoTimeoutException e) {
+ updateStatus(lastOperatingDataStatus = ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.unreachable");
} catch (IndegoException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
refreshOperatingDataConditionally(
previousDeviceStatus.isCharging() || deviceStatus.isCharging() || deviceStatus.isActive());
+ if (lastOperatingDataStatus == ThingStatus.ONLINE && thing.getStatus() != ThingStatus.ONLINE) {
+ // Revert temporary offline status caused by disruptions other than unreachable device.
+ updateStatus(ThingStatus.ONLINE);
+ } else if (lastOperatingDataStatus == ThingStatus.OFFLINE) {
+ // Update description to reflect why thing is still offline.
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/offline.comm-error.unreachable");
+ }
+
rescheduleStatePollAccordingToState(deviceStatus);
}
}
private void refreshOperatingDataConditionally(boolean isActive)
- throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
+ throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
// Refresh operating data only occationally or when robot is active/charging.
// This will contact the robot directly through cellular network and wake it up
- // when sleeping.
+ // when sleeping. Additionally, refresh more often after being offline to try to get
+ // back online as soon as possible without putting too much stress on the service.
if ((isActive && operatingDataTimestamp.isBefore(Instant.now().minus(OPERATING_DATA_ACTIVE_REFRESH_INTERVAL)))
+ || (lastOperatingDataStatus != ThingStatus.ONLINE && operatingDataTimestamp
+ .isBefore(Instant.now().minus(OPERATING_DATA_OFFLINE_REFRESH_INTERVAL)))
|| operatingDataTimestamp.isBefore(Instant.now().minus(OPERATING_DATA_INACTIVE_REFRESH_INTERVAL))) {
refreshOperatingData();
}
}
- private void refreshOperatingData()
- throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
+ private void refreshOperatingData() throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
updateOperatingData(controller.getOperatingData());
operatingDataTimestamp = Instant.now();
- updateStatus(ThingStatus.ONLINE);
+ updateStatus(lastOperatingDataStatus = ThingStatus.ONLINE);
}
private void refreshCuttingTimesWithExceptionHandling() {