Skip to content

Commit

Permalink
[entsoe] Refactor HTTP error handling (openhab#17616)
Browse files Browse the repository at this point in the history
* Refactor HTTP error handling

Signed-off-by: Jacob Laursen <[email protected]>
  • Loading branch information
jlaur authored and KaaNee committed Nov 8, 2024
1 parent 257b27d commit 2129dad
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@

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;
Expand All @@ -54,22 +55,18 @@
public class EntsoeHandler extends BaseThingHandler {

private final Logger logger = LoggerFactory.getLogger(EntsoeHandler.class);
private final ZoneId cetZoneId = ZoneId.of("CET");
private final Client client;

private EntsoeConfiguration config;

private EntsoeConfiguration config = new EntsoeConfiguration();
private @Nullable ScheduledFuture<?> refreshJob;

private Map<Instant, SpotPrice> entsoeTimeSeries = new LinkedHashMap<>();

private final ZoneId cetZoneId = ZoneId.of("CET");

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
Expand Down Expand Up @@ -101,7 +98,12 @@ public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("handleCommand(channelUID:{}, command:{})", channelUID.getAsString(), command.toFullString());

if (command instanceof RefreshType) {
fetchNewPrices();
try {
fetchNewPrices();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}

Expand Down Expand Up @@ -199,7 +201,11 @@ private void refreshPrices() {
}

if (entsoeTimeSeries.isEmpty()) {
fetchNewPrices();
try {
fetchNewPrices();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return;
}

Expand All @@ -211,27 +217,31 @@ private void refreshPrices() {
.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)
.minusDays(needToFetchHistoricDays() ? config.historicDays - 1 : 0).with(LocalTime.MIDNIGHT)
.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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@

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
Expand All @@ -33,6 +38,15 @@
@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);
Expand All @@ -43,7 +57,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();

if (THING_TYPE_DAY_AHEAD.equals(thingTypeUID)) {
return new EntsoeHandler(thing);
return new EntsoeHandler(thing, httpClient);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,25 @@
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;
Expand All @@ -46,27 +56,59 @@
@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) {
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: {}", responseText);
return parseXmlResponse(responseText, configResolution);
} catch (IOException e) {
String message = e.getMessage();
if (message != null && message.contains("Authentication challenge without WWW-Authenticate header")) {
throw new EntsoeConfigurationException("Authentication failed. Please check your security token");
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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*
*/
@NonNullByDefault
public class Request {
public class EntsoeRequest {

private static DateTimeFormatter requestFormat = DateTimeFormatter.ofPattern("yyyyMMddHHmm");

Expand All @@ -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;
Expand Down

0 comments on commit 2129dad

Please sign in to comment.