[entsoe] Refactor HTTP error handling (#17616)

* Refactor HTTP error handling

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Jacob Laursen 2024-10-29 23:03:53 +01:00 committed by Ciprian Pascu
parent 10054a7d5c
commit 9f5349bae8
4 changed files with 103 additions and 37 deletions

View File

@ -26,8 +26,9 @@ import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.entsoe.internal.client.Client;
import org.openhab.binding.entsoe.internal.client.Request;
import org.openhab.binding.entsoe.internal.client.EntsoeRequest;
import org.openhab.binding.entsoe.internal.client.SpotPrice;
import org.openhab.binding.entsoe.internal.exception.EntsoeConfigurationException;
import org.openhab.binding.entsoe.internal.exception.EntsoeResponseException;
@ -54,22 +55,18 @@ import org.slf4j.LoggerFactory;
public class EntsoeHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(EntsoeHandler.class);
private EntsoeConfiguration config;
private @Nullable ScheduledFuture<?> refreshJob;
private Map<Instant, SpotPrice> entsoeTimeSeries = new LinkedHashMap<>();
private final ZoneId cetZoneId = ZoneId.of("CET");
private final Client client;
private EntsoeConfiguration config = new EntsoeConfiguration();
private @Nullable ScheduledFuture<?> refreshJob;
private Map<Instant, SpotPrice> entsoeTimeSeries = new LinkedHashMap<>();
private ZonedDateTime lastDayAheadReceived = ZonedDateTime.of(LocalDateTime.MIN, cetZoneId);
private int historicDaysInitially = 0;
public EntsoeHandler(Thing thing) {
public EntsoeHandler(final Thing thing, final HttpClient httpClient) {
super(thing);
config = new EntsoeConfiguration();
this.client = new Client(httpClient);
}
@Override
@ -101,7 +98,12 @@ public class EntsoeHandler extends BaseThingHandler {
logger.trace("handleCommand(channelUID:{}, command:{})", channelUID.getAsString(), command.toFullString());
if (command instanceof RefreshType) {
fetchNewPrices();
try {
fetchNewPrices();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
@ -199,7 +201,11 @@ public class EntsoeHandler extends BaseThingHandler {
}
if (entsoeTimeSeries.isEmpty()) {
fetchNewPrices();
try {
fetchNewPrices();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return;
}
@ -211,14 +217,19 @@ public class EntsoeHandler extends BaseThingHandler {
.isAfter(currentCetTimeWholeHours().withHour(config.spotPricesAvailableCetHour));
if (needsInitialUpdate || (!hasNextDayValue && readyForNextDayValue)) {
fetchNewPrices();
try {
fetchNewPrices();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
} else {
updateCurrentState(EntsoeBindingConstants.CHANNEL_SPOT_PRICE);
schedule(true);
}
}
private void fetchNewPrices() {
private void fetchNewPrices() throws InterruptedException {
logger.trace("Fetching new prices");
Instant startUtc = ZonedDateTime.now(cetZoneId)
@ -226,12 +237,11 @@ public class EntsoeHandler extends BaseThingHandler {
.toInstant();
Instant endUtc = ZonedDateTime.now(cetZoneId).plusDays(2).with(LocalTime.MIDNIGHT).toInstant();
Request request = new Request(config.securityToken, config.area, startUtc, endUtc);
Client client = new Client();
EntsoeRequest request = new EntsoeRequest(config.securityToken, config.area, startUtc, endUtc);
boolean success = false;
try {
entsoeTimeSeries = client.doGetRequest(request, config.requestTimeout * 1000, config.resolution);
entsoeTimeSeries = client.doGetRequest(request, config.requestTimeout, config.resolution);
TimeSeries baseTimeSeries = new TimeSeries(EntsoeBindingConstants.TIMESERIES_POLICY);
for (Map.Entry<Instant, SpotPrice> entry : entsoeTimeSeries.entrySet()) {

View File

@ -16,12 +16,17 @@ import static org.openhab.binding.entsoe.internal.EntsoeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link EntsoeHandlerFactory} is responsible for creating things and thing
@ -33,6 +38,15 @@ import org.osgi.service.component.annotations.Component;
@Component(configurationPid = "binding.entsoe", service = ThingHandlerFactory.class)
public class EntsoeHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient;
@Activate
public EntsoeHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
ComponentContext componentContext) {
super.activate(componentContext);
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID);
@ -43,7 +57,7 @@ public class EntsoeHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_DAY_AHEAD.equals(thingTypeUID)) {
return new EntsoeHandler(thing);
return new EntsoeHandler(thing, httpClient);
}
return null;

View File

@ -21,15 +21,25 @@ import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpResponseException;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.entsoe.internal.exception.EntsoeConfigurationException;
import org.openhab.binding.entsoe.internal.exception.EntsoeResponseException;
import org.openhab.core.io.net.http.HttpUtil;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
@ -46,27 +56,59 @@ import org.xml.sax.SAXException;
@NonNullByDefault
public class Client {
private final Logger logger = LoggerFactory.getLogger(Client.class);
private final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
private final HttpClient httpClient;
private final String userAgent;
private DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
public Client(HttpClient httpClient) {
this.httpClient = httpClient;
userAgent = "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString();
}
public Map<Instant, SpotPrice> doGetRequest(EntsoeRequest entsoeRequest, int timeout, String configResolution)
throws EntsoeResponseException, EntsoeConfigurationException, InterruptedException {
String url = entsoeRequest.toUrl();
Request request = httpClient.newRequest(url) //
.timeout(timeout, TimeUnit.SECONDS) //
.agent(userAgent) //
.method(HttpMethod.GET);
public Map<Instant, SpotPrice> doGetRequest(Request request, int timeout, String configResolution)
throws EntsoeResponseException, EntsoeConfigurationException {
try {
logger.debug("Sending GET request with parameters: {}", request);
String url = request.toUrl();
String responseText = HttpUtil.executeUrl("GET", url, timeout);
if (responseText == null) {
throw new EntsoeResponseException("Request failed");
}
logger.trace("Response: {}", responseText);
return parseXmlResponse(responseText, configResolution);
} catch (IOException e) {
String message = e.getMessage();
if (message != null && message.contains("Authentication challenge without WWW-Authenticate header")) {
logger.debug("Sending GET request with parameters: {}", entsoeRequest);
ContentResponse response = request.send();
int status = response.getStatus();
if (status == HttpStatus.UNAUTHORIZED_401) {
// This will currently not happen because "WWW-Authenticate" header is missing; see below.
throw new EntsoeConfigurationException("Authentication failed. Please check your security token");
}
if (!HttpStatus.isSuccess(status)) {
throw new EntsoeResponseException("The request failed with HTTP error " + status);
}
String responseContent = response.getContentAsString();
if (responseContent == null) {
throw new EntsoeResponseException("Request failed");
}
logger.trace("Response: {}", responseContent);
return parseXmlResponse(responseContent, configResolution);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause != null && cause instanceof HttpResponseException httpResponseException) {
Response response = httpResponseException.getResponse();
if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
/*
* The service may respond with HTTP code 401 without any "WWW-Authenticate"
* header, violating RFC 7235. Jetty will then throw HttpResponseException.
* We need to handle this in order to attempt reauthentication.
*/
throw new EntsoeConfigurationException("Authentication failed. Please check your security token");
}
}
throw new EntsoeResponseException(e);
} catch (ParserConfigurationException | SAXException e) {
} catch (IOException | TimeoutException | ParserConfigurationException | SAXException e) {
throw new EntsoeResponseException(e);
}
}

View File

@ -23,7 +23,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*
*/
@NonNullByDefault
public class Request {
public class EntsoeRequest {
private static DateTimeFormatter requestFormat = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
@ -35,7 +35,7 @@ public class Request {
private final Instant periodStart;
private final Instant periodEnd;
public Request(String securityToken, String area, Instant periodStart, Instant periodEnd) {
public EntsoeRequest(String securityToken, String area, Instant periodStart, Instant periodEnd) {
this.securityToken = securityToken;
this.area = area;
this.periodStart = periodStart;