diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java index 2a65d9a841..a8c224ffe8 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/ApiController.java @@ -14,6 +14,7 @@ package org.openhab.binding.energidataservice.internal; import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*; +import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -53,6 +54,7 @@ import org.openhab.binding.energidataservice.internal.api.dto.ElspotpriceRecords import org.openhab.binding.energidataservice.internal.api.serialization.InstantDeserializer; import org.openhab.binding.energidataservice.internal.api.serialization.LocalDateTimeDeserializer; import org.openhab.binding.energidataservice.internal.exception.DataServiceException; +import org.openhab.binding.energidataservice.internal.exception.DataServiceRateLimitException; import org.openhab.core.i18n.TimeZoneProvider; import org.osgi.framework.FrameworkUtil; import org.slf4j.Logger; @@ -198,10 +200,34 @@ public class ApiController { updatePropertiesFromResponse(response, properties); int status = response.getStatus(); + String responseContent = response.getContentAsString(); + if (!HttpStatus.isSuccess(status)) { + if (logger.isTraceEnabled()) { + logger.trace("Request failed with HTTP error {}: {}", status, responseContent); + logger.trace("Response headers: {}", response.getHeaders()); + } + + if (status == HttpStatus.TOO_MANY_REQUESTS_429) { + String retryAfter = response.getHeaders().get("Retry-After"); + if (retryAfter != null) { + try { + int retryAfterSeconds = Integer.parseInt(retryAfter); + if (retryAfterSeconds < 0) { + logger.debug("Invalid Retry-After header value: '{}'", retryAfter); + throw new DataServiceRateLimitException("Rate limit is exceeded"); + } + throw new DataServiceRateLimitException( + "Rate limit is exceeded. Retrying after " + retryAfter + " seconds.", + Duration.ofSeconds(retryAfterSeconds)); + } catch (NumberFormatException e) { + logger.debug("Invalid Retry-After header value: '{}'", retryAfter); + throw new DataServiceRateLimitException("Rate limit is exceeded", e); + } + } + } throw new DataServiceException("The request failed with HTTP error " + status, status); } - String responseContent = response.getContentAsString(); if (responseContent.isEmpty()) { throw new DataServiceException("Empty response"); } diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/console/EnergiDataServiceCommandExtension.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/console/EnergiDataServiceCommandExtension.java index fa60be07b2..848cd88ec2 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/console/EnergiDataServiceCommandExtension.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/console/EnergiDataServiceCommandExtension.java @@ -16,7 +16,6 @@ import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.format.DateTimeParseException; -import java.util.Arrays; import java.util.List; import java.util.stream.Stream; @@ -161,7 +160,7 @@ public class EnergiDataServiceCommandExtension extends AbstractConsoleCommandExt @Override public List getUsages() { - return Arrays.asList(buildCommandUsage(SUBCMD_UPDATE + " [" + return List.of(buildCommandUsage(SUBCMD_UPDATE + " [" + String.join("|", Stream.of(PriceComponent.values()).map(PriceComponent::toString).toList()) + "] []", "Update time series in requested period")); } diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceRateLimitException.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceRateLimitException.java new file mode 100644 index 0000000000..18123bede0 --- /dev/null +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/exception/DataServiceRateLimitException.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010-2026 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.energidataservice.internal.exception; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.http.HttpStatus; + +/** + * {@link DataServiceRateLimitException} is a specialized Energi Data Service exception + * thrown in case of exceeding the API rate limit. It contains information about how long to + * wait before retrying the request. + * + * @see Energi Data Service API documentation on rate + * limiting + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class DataServiceRateLimitException extends DataServiceException { + + private static final Duration DEFAULT_RETRY_AFTER = Duration.ofMinutes(30); + + private static final long serialVersionUID = 1L; + + private final Duration retryAfter; + + public DataServiceRateLimitException(String message) { + this(message, DEFAULT_RETRY_AFTER); + } + + public DataServiceRateLimitException(String message, Throwable cause) { + super(message, HttpStatus.TOO_MANY_REQUESTS_429); + initCause(cause); + this.retryAfter = DEFAULT_RETRY_AFTER; + } + + public DataServiceRateLimitException(String message, Duration retryAfter) { + super(message, HttpStatus.TOO_MANY_REQUESTS_429); + this.retryAfter = retryAfter; + } + + public Duration getRetryAfter() { + return retryAfter; + } +} diff --git a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java index f637afa4d8..5080f10e27 100644 --- a/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java +++ b/bundles/org.openhab.binding.energidataservice/src/main/java/org/openhab/binding/energidataservice/internal/retry/RetryPolicyFactory.java @@ -20,6 +20,7 @@ import java.time.ZoneId; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jetty.http.HttpStatus; import org.openhab.binding.energidataservice.internal.exception.DataServiceException; +import org.openhab.binding.energidataservice.internal.exception.DataServiceRateLimitException; import org.openhab.binding.energidataservice.internal.retry.strategy.ExponentialBackoff; import org.openhab.binding.energidataservice.internal.retry.strategy.FixedTime; import org.openhab.binding.energidataservice.internal.retry.strategy.Linear; @@ -40,16 +41,20 @@ public class RetryPolicyFactory { * @return retry strategy */ public static RetryStrategy fromThrowable(Throwable e) { - if (e instanceof DataServiceException dse) { - switch (dse.getHttpStatus()) { - case HttpStatus.TOO_MANY_REQUESTS_429: - return new ExponentialBackoff().withMinimum(Duration.ofMinutes(30)); - default: - return new ExponentialBackoff().withMinimum(Duration.ofMinutes(1)).withJitter(0.2); - } - } + ExponentialBackoff strategy = switch (e) { + case DataServiceRateLimitException rle -> + new ExponentialBackoff() + .withMinimum(rle.getRetryAfter()); - return new ExponentialBackoff().withMinimum(Duration.ofMinutes(1)).withJitter(0.2); + case DataServiceException dse when dse.getHttpStatus() == HttpStatus.TOO_MANY_REQUESTS_429 -> + new ExponentialBackoff() + .withMinimum(Duration.ofMinutes(30)); + + default -> new ExponentialBackoff() + .withMinimum(Duration.ofMinutes(1)); + }; + + return strategy.withJitter(0.2); } /**