diff --git a/java-client/src/main/java/energy/trolie/client/TrolieApiConstants.java b/java-client/src/main/java/energy/trolie/client/TrolieApiConstants.java index aa09429..194cba1 100644 --- a/java-client/src/main/java/energy/trolie/client/TrolieApiConstants.java +++ b/java-client/src/main/java/energy/trolie/client/TrolieApiConstants.java @@ -8,6 +8,11 @@ public class TrolieApiConstants { private TrolieApiConstants() { } + /** + * Path to getSeasonalRatings + */ + public static final String PATH_SEASONAL_SNAPSHOT = "/seasonal-ratings/snapshot"; + /** * Path to getRealTimeLimits */ @@ -48,6 +53,11 @@ private TrolieApiConstants() { */ public static final String PATH_DEFAULT_MONITORING_SET = "/default-monitoring-set"; + /** + * Content type for seasonal rating snapshots + */ + public static final String CONTENT_TYPE_SEASONAL_SNAPSHOT = "application/vnd.trolie.seasonal-rating-snapshot.v1+json"; + /** * Content type for real-time limit snapshots */ diff --git a/java-client/src/main/java/energy/trolie/client/TrolieClient.java b/java-client/src/main/java/energy/trolie/client/TrolieClient.java index fc790d1..17b895b 100644 --- a/java-client/src/main/java/energy/trolie/client/TrolieClient.java +++ b/java-client/src/main/java/energy/trolie/client/TrolieClient.java @@ -6,6 +6,8 @@ import energy.trolie.client.request.operatingsnapshots.ForecastSnapshotSubscribedReceiver; import energy.trolie.client.request.operatingsnapshots.RealTimeSnapshotReceiver; import energy.trolie.client.request.operatingsnapshots.RealTimeSnapshotSubscribedReceiver; +import energy.trolie.client.request.operatingsnapshots.SeasonalSnapshotReceiver; +import energy.trolie.client.request.operatingsnapshots.SeasonalSnapshotSubscribedReceiver; import energy.trolie.client.request.ratingproposals.ForecastRatingProposalUpdate; import energy.trolie.client.request.ratingproposals.RealTimeRatingProposalUpdate; import lombok.NonNull; @@ -385,6 +387,60 @@ RequestSubscription subscribeToRegionalRealTimeLimits( */ RealTimeRatingProposalUpdate createRealTimeRatingProposalStreamingUpdate(); + /** + * Execute a synchronous + * request for the current seasonal limits with a streaming response handler, + * assuming this user's default monitoring set. + * + * @param receiver Streaming data receiver for snapshot data + */ + void getInUseSeasonalSnapshots( + SeasonalSnapshotReceiver receiver); + + /** + * Execute a request for the current seasonal limits with a streaming response handler + * with the given monitoring set. + * + * @param receiver Streaming data receiver for snapshot data + * @param monitoringSet filter for monitoring set name + */ + void getInUseSeasonalSnapshots( + SeasonalSnapshotReceiver receiver, + String monitoringSet); + + /** + * Execute a request for the current seasonal limits with a streaming response handler + * + * @param receiver Streaming data receiver for snapshot data + * @param monitoringSet filter for monitoring set name + * @param resourceId Only return snapshots for this power system resource + */ + void getInUseSeasonalSnapshots( + SeasonalSnapshotReceiver receiver, + String monitoringSet, + String resourceId + ); + + /** + * Create a polling subscription for seasonal limits data updates + * + * @param receiver Streaming data receiver for snapshot data + * @param monitoringSet filter for monitoring set name + * @return request handle + */ + RequestSubscription subscribeToInUseSeasonalSnapshotUpdates( + SeasonalSnapshotSubscribedReceiver receiver, + String monitoringSet); + + /** + * Create a polling subscription for seasonal snapshot data updates + * + * @param receiver Streaming data receiver for snapshot data + * @return request handle + */ + RequestSubscription subscribeToInUseSeasonalSnapshotUpdates( + SeasonalSnapshotSubscribedReceiver receiver); + /** * Un-subscribe an active polling request * diff --git a/java-client/src/main/java/energy/trolie/client/TrolieClientBuilder.java b/java-client/src/main/java/energy/trolie/client/TrolieClientBuilder.java index 77988d5..0700e25 100644 --- a/java-client/src/main/java/energy/trolie/client/TrolieClientBuilder.java +++ b/java-client/src/main/java/energy/trolie/client/TrolieClientBuilder.java @@ -7,6 +7,7 @@ import energy.trolie.client.impl.TrolieClientImpl; import energy.trolie.client.request.monitoringsets.MonitoringSetsSubscribedReceiver; import energy.trolie.client.request.operatingsnapshots.ForecastSnapshotSubscribedReceiver; +import energy.trolie.client.request.operatingsnapshots.SeasonalSnapshotSubscribedReceiver; import energy.trolie.client.request.operatingsnapshots.RealTimeSnapshotSubscribedReceiver; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -54,6 +55,7 @@ public class TrolieClientBuilder { private int realTimeRatingsPollMs = 10000; private int forecastRatingsPollMs = 30000; private int monitoringSetPollMs = 60000; + private int seasonalRatingsPollMs = 30000; /** * Initializes a new builder with a preconfigured apache HTTP client @@ -178,6 +180,18 @@ public TrolieClientBuilder monitoringSetPollMs(int monitoringSetPollMs) { return this; } + /** + * Sets the period at which + * {@link TrolieClient#subscribeToInUseSeasonalSnapshotUpdates(SeasonalSnapshotSubscribedReceiver)} + * and similar methods for forecast ratings poll for new ratings. Defaults to 30 seconds + * @param seasonalRatingsPollMs new poll periodicity in milliseconds + * @return fluent builder + */ + public TrolieClientBuilder seasonalRatingsPollMs(int seasonalRatingsPollMs) { + this.seasonalRatingsPollMs = seasonalRatingsPollMs; + return this; + } + /** * Construct a new client * @return new client @@ -204,6 +218,6 @@ public TrolieClient build() { return new TrolieClientImpl(httpClient, host, requestConfig, bufferSize, objectMapper, eTagStore, httpHeaders, periodLengthMinutes, realTimeRatingsPollMs, - forecastRatingsPollMs, monitoringSetPollMs); + forecastRatingsPollMs, monitoringSetPollMs, seasonalRatingsPollMs); } } diff --git a/java-client/src/main/java/energy/trolie/client/impl/TrolieClientImpl.java b/java-client/src/main/java/energy/trolie/client/impl/TrolieClientImpl.java index 8ba7ef2..807c92e 100644 --- a/java-client/src/main/java/energy/trolie/client/impl/TrolieClientImpl.java +++ b/java-client/src/main/java/energy/trolie/client/impl/TrolieClientImpl.java @@ -18,12 +18,16 @@ import energy.trolie.client.impl.request.operatingsnapshots.RegionalForecastSubscribedSnapshotRequest; import energy.trolie.client.impl.request.operatingsnapshots.RegionalRealTimeSnapshotRequest; import energy.trolie.client.impl.request.operatingsnapshots.RegionalRealTimeSnapshotSubscribedRequest; +import energy.trolie.client.impl.request.operatingsnapshots.SeasonalSnapshotRequest; +import energy.trolie.client.impl.request.operatingsnapshots.SeasonalSnapshotSubscribedRequest; import energy.trolie.client.request.monitoringsets.MonitoringSetsReceiver; import energy.trolie.client.request.monitoringsets.MonitoringSetsSubscribedReceiver; import energy.trolie.client.request.operatingsnapshots.ForecastSnapshotReceiver; import energy.trolie.client.request.operatingsnapshots.ForecastSnapshotSubscribedReceiver; import energy.trolie.client.request.operatingsnapshots.RealTimeSnapshotReceiver; import energy.trolie.client.request.operatingsnapshots.RealTimeSnapshotSubscribedReceiver; +import energy.trolie.client.request.operatingsnapshots.SeasonalSnapshotReceiver; +import energy.trolie.client.request.operatingsnapshots.SeasonalSnapshotSubscribedReceiver; import energy.trolie.client.request.ratingproposals.ForecastRatingProposalUpdate; import energy.trolie.client.request.ratingproposals.RealTimeRatingProposalUpdate; import org.apache.hc.client5.http.config.RequestConfig; @@ -54,13 +58,15 @@ public class TrolieClientImpl implements TrolieClient { private final int realTimeRatingsPollMs; private final int forecastRatingsPollMs; private final int monitoringSetPollMs; + private final int seasonalRatingsPollMs; public TrolieClientImpl(CloseableHttpClient httpClient, TrolieHost host, RequestConfig requestConfig, int bufferSize, ObjectMapper objectMapper, ETagStore eTagStore, Map httpHeaders, int defaultIntervalMinutes, int realTimeRatingsPollMs, int forecastRatingsPollMs, - int monitoringSetPollMs) { + int monitoringSetPollMs, + int seasonalRatingsPollMs) { super(); this.httpClient = httpClient; this.host = host; @@ -73,6 +79,7 @@ public TrolieClientImpl(CloseableHttpClient httpClient, TrolieHost host, Request this.realTimeRatingsPollMs = realTimeRatingsPollMs; this.forecastRatingsPollMs = forecastRatingsPollMs; this.monitoringSetPollMs = monitoringSetPollMs; + this.seasonalRatingsPollMs = seasonalRatingsPollMs; } final Set activeSubscriptions = new HashSet<>(); @@ -373,6 +380,63 @@ public DefaultMonitoringSetSubscribedRequest subscribeToDefaultMonitoringSetUpda return subscription; } + @Override + public void getInUseSeasonalSnapshots(SeasonalSnapshotReceiver receiver) { + getInUseSeasonalSnapshots(receiver, null, null); + } + + @Override + public void getInUseSeasonalSnapshots(SeasonalSnapshotReceiver receiver, String monitoringSet) { + getInUseSeasonalSnapshots(receiver, monitoringSet, null); + } + + @Override + public void getInUseSeasonalSnapshots( + SeasonalSnapshotReceiver receiver, + String monitoringSet, + String resourceId) { + + new SeasonalSnapshotRequest( + httpClient, + host, + requestConfig, + bufferSize, + objectMapper, + httpHeaders, + receiver, + monitoringSet, + resourceId).executeRequest(); + } + + @Override + public SeasonalSnapshotSubscribedRequest subscribeToInUseSeasonalSnapshotUpdates( + SeasonalSnapshotSubscribedReceiver receiver) { + return subscribeToInUseSeasonalSnapshotUpdates(receiver, null); + } + + @Override + public SeasonalSnapshotSubscribedRequest subscribeToInUseSeasonalSnapshotUpdates( + SeasonalSnapshotSubscribedReceiver receiver, + String monitoringSet) { + + SeasonalSnapshotSubscribedRequest subscription = new SeasonalSnapshotSubscribedRequest( + httpClient, + host, + requestConfig, + bufferSize, + objectMapper, + httpHeaders, + seasonalRatingsPollMs, + receiver, + eTagStore, + monitoringSet); + + addSubscription(subscription); + return subscription; + + } + + @Override public void close() throws IOException { logger.info("Closing all subscriptions"); diff --git a/java-client/src/main/java/energy/trolie/client/impl/request/operatingsnapshots/SeasonalSnapshotRequest.java b/java-client/src/main/java/energy/trolie/client/impl/request/operatingsnapshots/SeasonalSnapshotRequest.java new file mode 100644 index 0000000..af51080 --- /dev/null +++ b/java-client/src/main/java/energy/trolie/client/impl/request/operatingsnapshots/SeasonalSnapshotRequest.java @@ -0,0 +1,78 @@ +package energy.trolie.client.impl.request.operatingsnapshots; + +import com.fasterxml.jackson.databind.ObjectMapper; +import energy.trolie.client.TrolieApiConstants; +import energy.trolie.client.TrolieHost; +import energy.trolie.client.impl.request.AbstractStreamingGet; +import energy.trolie.client.request.operatingsnapshots.SeasonalSnapshotReceiver; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.net.URIBuilder; + +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.Map; + +/** + * On-demand GET request for seasonal limits with ETAG usage + */ +public class SeasonalSnapshotRequest extends AbstractStreamingGet { + + String monitoringSet; + String resourceId; + + public SeasonalSnapshotRequest( + HttpClient httpClient, + TrolieHost host, + RequestConfig requestConfig, + int bufferSize, + ObjectMapper objectMapper, + Map httpHeaders, + SeasonalSnapshotReceiver receiver, // seasonal + String monitoringSet, + String resourceId) { + + super(httpClient, host, requestConfig, bufferSize, objectMapper, httpHeaders, receiver); + this.monitoringSet = monitoringSet; + this.resourceId = resourceId; + } + + @Override + protected String getPath() { + return TrolieApiConstants.PATH_SEASONAL_SNAPSHOT; + } + + @Override + protected String getContentType() { + return TrolieApiConstants.CONTENT_TYPE_SEASONAL_SNAPSHOT; + } + + @Override + protected HttpGet createRequest() throws URISyntaxException { + + HttpGet get = super.createRequest(); + URIBuilder uriBuilder = new URIBuilder(get.getUri()); + + if (monitoringSet != null) { + + //add the monitoring set parameter to the base URI + uriBuilder.addParameter(TrolieApiConstants.PARAM_MONITORING_SET, monitoringSet); + } + + if (resourceId != null) { + //add the resource ID parameter to the base URI + uriBuilder.addParameter(TrolieApiConstants.PARAM_RESOURCE_ID, resourceId); + } + + get.setUri(uriBuilder.build()); + return get; + } + + @Override + protected Boolean handleResponseContent(InputStream inputStream) { + return new SeasonalSnapshotResponseParser(receiver).parseResponse(inputStream, jsonFactory); + } + + +} diff --git a/java-client/src/main/java/energy/trolie/client/impl/request/operatingsnapshots/SeasonalSnapshotResponseParser.java b/java-client/src/main/java/energy/trolie/client/impl/request/operatingsnapshots/SeasonalSnapshotResponseParser.java new file mode 100644 index 0000000..21b4683 --- /dev/null +++ b/java-client/src/main/java/energy/trolie/client/impl/request/operatingsnapshots/SeasonalSnapshotResponseParser.java @@ -0,0 +1,96 @@ +package energy.trolie.client.impl.request.operatingsnapshots; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import energy.trolie.client.exception.StreamingGetConnectionException; +import energy.trolie.client.exception.StreamingGetHandlingException; +import energy.trolie.client.model.operatingsnapshots.SeasonalPeriodSnapshot; +import energy.trolie.client.model.operatingsnapshots.SeasonalSnapshotHeader; +import energy.trolie.client.request.operatingsnapshots.SeasonalSnapshotReceiver; +import lombok.AllArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Implementation for parsing a seasonal snapshot response shared between subscribed and on-demand requests + */ +@AllArgsConstructor +public class SeasonalSnapshotResponseParser { + + private static final Logger logger = LoggerFactory.getLogger(SeasonalSnapshotResponseParser.class); + + SeasonalSnapshotReceiver receiver; + + public Boolean parseResponse(InputStream inputStream, JsonFactory jsonFactory) { + + try (JsonParser parser = jsonFactory.createParser(inputStream);) { + + receiver.beginSnapshot(); + + //START_OBJECT seasonal + parser.nextToken(); + //FIELD_NAME snapshot-header + parser.nextToken(); + //START_OBJECT header + parser.nextToken(); + + //read header + SeasonalSnapshotHeader header = parser.readValueAs(SeasonalSnapshotHeader.class); + receiver.header(header); + + //FIELD_NAME ratings + parser.nextToken(); + //START_ARRAY + parser.nextToken(); + + //for each rating + while (parser.nextToken() == JsonToken.START_OBJECT ) { + + //FIELD_NAME resource-id + parser.nextToken(); + //resource-id + parser.nextToken(); + + String id = parser.getValueAsString(); + receiver.beginResource(id); + + //FIELD_NAME periods + parser.nextToken(); + //START_ARRAY + parser.nextToken(); + + //for each period + while (parser.nextToken() == JsonToken.START_OBJECT ) { + SeasonalPeriodSnapshot period = parser.readValueAs(SeasonalPeriodSnapshot.class); + receiver.period(period); + } + //exit loop on END_ARRAY periods + + //END_OBJECT rating + parser.nextToken(); + receiver.endResource(); + + //next token will be START_OBJECT | END_ARRAY ratings + + } + //exit loop on END_ARRAY ratings + + receiver.endSnapshot(); + return true; + + } catch (IOException e) { + logger.error("I/O error handling response",e); + receiver.error(new StreamingGetConnectionException(e)); + } catch (Exception e) { + logger.error("Error handling response data",e); + receiver.error(new StreamingGetHandlingException(e)); + } + + return false; + } + +} diff --git a/java-client/src/main/java/energy/trolie/client/impl/request/operatingsnapshots/SeasonalSnapshotSubscribedRequest.java b/java-client/src/main/java/energy/trolie/client/impl/request/operatingsnapshots/SeasonalSnapshotSubscribedRequest.java new file mode 100644 index 0000000..c44169f --- /dev/null +++ b/java-client/src/main/java/energy/trolie/client/impl/request/operatingsnapshots/SeasonalSnapshotSubscribedRequest.java @@ -0,0 +1,75 @@ +package energy.trolie.client.impl.request.operatingsnapshots; + +import com.fasterxml.jackson.databind.ObjectMapper; +import energy.trolie.client.ETagStore; +import energy.trolie.client.TrolieApiConstants; +import energy.trolie.client.TrolieHost; +import energy.trolie.client.impl.request.AbstractStreamingSubscribedGet; +import energy.trolie.client.request.operatingsnapshots.SeasonalSnapshotSubscribedReceiver; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.net.URIBuilder; + +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.Map; + +/** + * subscription for seasonal rating snapshots + */ +public class SeasonalSnapshotSubscribedRequest extends AbstractStreamingSubscribedGet { + + String monitoringSet; + + public SeasonalSnapshotSubscribedRequest( + HttpClient httpClient, + TrolieHost host, + RequestConfig requestConfig, + int bufferSize, + ObjectMapper objectMapper, + Map httpHeaders, + int pollingRateMillis, + SeasonalSnapshotSubscribedReceiver receiver, + ETagStore eTagStore, + String monitoringSet) { + + super(httpClient, host, requestConfig, bufferSize, objectMapper, httpHeaders, pollingRateMillis, + receiver, eTagStore); + this.monitoringSet = monitoringSet; + } + + @Override + protected String getPath() { + return TrolieApiConstants.PATH_SEASONAL_SNAPSHOT; + } + + @Override + protected String getContentType() { + return TrolieApiConstants.CONTENT_TYPE_SEASONAL_SNAPSHOT; + } + + @Override + protected HttpGet createRequest() throws URISyntaxException { + + HttpGet get = super.createRequest(); + + if (monitoringSet != null) { + + //add the monitoring set parameter to the base URI + URIBuilder uriBuilder = new URIBuilder(get.getUri()) + .addParameter(TrolieApiConstants.PARAM_MONITORING_SET, monitoringSet); + get.setUri(uriBuilder.build()); + } + + return get; + } + + @Override + protected Boolean handleResponseContent(InputStream inputStream) { + + return new SeasonalSnapshotResponseParser(receiver).parseResponse(inputStream, jsonFactory); + + } + +} diff --git a/java-client/src/main/java/energy/trolie/client/model/operatingsnapshots/SeasonalPeriodSnapshot.java b/java-client/src/main/java/energy/trolie/client/model/operatingsnapshots/SeasonalPeriodSnapshot.java new file mode 100644 index 0000000..fcd6df7 --- /dev/null +++ b/java-client/src/main/java/energy/trolie/client/model/operatingsnapshots/SeasonalPeriodSnapshot.java @@ -0,0 +1,59 @@ +/* + * =========================================================================== + * + * Copyright and Proprietary Information + * + * Copyright (c) 1993, 2024 General Electric Technology GmbH. All Rights Reserved. + * + * NOTE: CONTAINS CONFIDENTIAL AND PROPRIETARY INFORMATION OF GENERAL ELECTRIC + * TECHNOLOGY GMBH AND MAY NOT BE REPRODUCED, TRANSMITTED, STORED, OR COPIED IN + * WHOLE OR IN PART, OR USED TO FURNISH INFORMATION TO OTHERS, WITHOUT THE PRIOR + * WRITTEN PERMISSION OF GENERAL ELECTRIC TECHNOLOGY GMBH OR GRID SOLUTIONS. + * + * ========================================================================== + */ + +package energy.trolie.client.model.operatingsnapshots; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import energy.trolie.client.model.common.EmergencyRatingValue; +import energy.trolie.client.model.common.RatingValue; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.Instant; +import java.util.List; + +/** + * For a given resource, represents a rating value set for a given season. + */ +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@EqualsAndHashCode +public class SeasonalPeriodSnapshot { + + @JsonFormat(shape = JsonFormat.Shape.STRING) + @JsonProperty("period-start") + private Instant periodStart; + + @JsonFormat(shape = JsonFormat.Shape.STRING) + @JsonProperty("period-end") + private Instant periodEnd; + + @JsonProperty("season-name") + private String seasonName; + + @JsonProperty("continuous-operating-limit") + private RatingValue continuousOperatingLimit; + + @JsonProperty("emergency-operating-limits") + private List emergencyOperatingLimits; +} diff --git a/java-client/src/main/java/energy/trolie/client/model/operatingsnapshots/SeasonalSnapshotHeader.java b/java-client/src/main/java/energy/trolie/client/model/operatingsnapshots/SeasonalSnapshotHeader.java new file mode 100644 index 0000000..7449653 --- /dev/null +++ b/java-client/src/main/java/energy/trolie/client/model/operatingsnapshots/SeasonalSnapshotHeader.java @@ -0,0 +1,35 @@ +/* + * =========================================================================== + * + * Copyright and Proprietary Information + * + * Copyright (c) 1993, 2024 General Electric Technology GmbH. All Rights Reserved. + * + * NOTE: CONTAINS CONFIDENTIAL AND PROPRIETARY INFORMATION OF GENERAL ELECTRIC + * TECHNOLOGY GMBH AND MAY NOT BE REPRODUCED, TRANSMITTED, STORED, OR COPIED IN + * WHOLE OR IN PART, OR USED TO FURNISH INFORMATION TO OTHERS, WITHOUT THE PRIOR + * WRITTEN PERMISSION OF GENERAL ELECTRIC TECHNOLOGY GMBH OR GRID SOLUTIONS. + * + * ========================================================================== + */ + +package energy.trolie.client.model.operatingsnapshots; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +/** + * {@link SnapshotHeader} for seasonal rating snapshots + */ +@AllArgsConstructor +@SuperBuilder +@Getter +@ToString +@EqualsAndHashCode(callSuper = true) +public class SeasonalSnapshotHeader extends SnapshotHeader { + + +} diff --git a/java-client/src/main/java/energy/trolie/client/request/operatingsnapshots/SeasonalSnapshotReceiver.java b/java-client/src/main/java/energy/trolie/client/request/operatingsnapshots/SeasonalSnapshotReceiver.java new file mode 100644 index 0000000..d5b0638 --- /dev/null +++ b/java-client/src/main/java/energy/trolie/client/request/operatingsnapshots/SeasonalSnapshotReceiver.java @@ -0,0 +1,60 @@ +package energy.trolie.client.request.operatingsnapshots; + +import energy.trolie.client.StreamingResponseReceiver; +import energy.trolie.client.exception.StreamingGetException; +import energy.trolie.client.model.operatingsnapshots.ForecastPeriodSnapshot; +import energy.trolie.client.model.operatingsnapshots.ForecastSnapshotHeader; +import energy.trolie.client.model.operatingsnapshots.SeasonalPeriodSnapshot; +import energy.trolie.client.model.operatingsnapshots.SeasonalSnapshotHeader; + +/** + * Streaming receiver for updated seasonal snapshot data and errors from subscriber. + * The current request handling can be terminated in any of these methods by throwing an exception. + * Errors originating from the subscriber thread will be sent to {@link #error(StreamingGetException)} + */ +public interface SeasonalSnapshotReceiver extends StreamingResponseReceiver { + + /** + * Invoked when a new snapshot is received. This will be the first method invoked when the snapshot is received. + */ + void beginSnapshot(); + + /** + * Invoked when the header has been processed. Will + * be invoked before processing resources + * @param header parsed header + */ + void header(SeasonalSnapshotHeader header); + + /** + * Invoked to indicate that a new resource has been found while parsing. + * Implementations may assume that all period ratings for this resource will + * be consumed through invocations to {@link #period(SeasonalPeriodSnapshot)} + * until {@link #endResource()} is invoked. + * @param resourceId the next resource Id. + */ + void beginResource(String resourceId); + + /** + * Invoked as a new rating value set is encountered in the stream + * @param period rating data for a given period. The resource ID + * is implied from the context, assuming the last call + * to {@link #beginResource(String)}. + */ + void period(SeasonalPeriodSnapshot period); + + /** + * Invoked when the end of a set of ratings for the current resource has been encountered. + * Users may assume that the last resource set by {@link #beginResource(String)} is no longer valid. + */ + void endResource(); + + /** + * Invoked when the snapshot has reached its end. + */ + void endSnapshot(); + +} + + + diff --git a/java-client/src/main/java/energy/trolie/client/request/operatingsnapshots/SeasonalSnapshotSubscribedReceiver.java b/java-client/src/main/java/energy/trolie/client/request/operatingsnapshots/SeasonalSnapshotSubscribedReceiver.java new file mode 100644 index 0000000..5f5b4cd --- /dev/null +++ b/java-client/src/main/java/energy/trolie/client/request/operatingsnapshots/SeasonalSnapshotSubscribedReceiver.java @@ -0,0 +1,16 @@ +package energy.trolie.client.request.operatingsnapshots; + +import energy.trolie.client.StreamingSubscribedResponseReceiver; +import energy.trolie.client.exception.StreamingGetException; + +/** + * Streaming receiver for updated seasonal snapshot data and errors from subscriber. + * The current request handling can be terminated in any of these methods by throwing an exception. + * Errors originating from the subscriber thread will be sent to {@link #error(StreamingGetException)} + */ +public interface SeasonalSnapshotSubscribedReceiver extends StreamingSubscribedResponseReceiver, SeasonalSnapshotReceiver { + +} + + + diff --git a/java-client/src/test/java/energy/trolie/client/TrolieClientIT.java b/java-client/src/test/java/energy/trolie/client/TrolieClientIT.java index 5fb5c75..688252b 100644 --- a/java-client/src/test/java/energy/trolie/client/TrolieClientIT.java +++ b/java-client/src/test/java/energy/trolie/client/TrolieClientIT.java @@ -15,6 +15,8 @@ import energy.trolie.client.model.operatingsnapshots.ForecastSnapshotHeader; import energy.trolie.client.model.operatingsnapshots.RealTimeLimit; import energy.trolie.client.model.operatingsnapshots.RealTimeSnapshotHeader; +import energy.trolie.client.model.operatingsnapshots.SeasonalPeriodSnapshot; +import energy.trolie.client.model.operatingsnapshots.SeasonalSnapshotHeader; import energy.trolie.client.model.ratingproposals.ForecastProposalHeader; import energy.trolie.client.model.ratingproposals.ForecastRatingPeriod; import energy.trolie.client.model.ratingproposals.ForecastRatingProposalStatus; @@ -27,6 +29,8 @@ import energy.trolie.client.request.operatingsnapshots.ForecastSnapshotSubscribedReceiver; import energy.trolie.client.request.operatingsnapshots.RealTimeSnapshotReceiver; import energy.trolie.client.request.operatingsnapshots.RealTimeSnapshotSubscribedReceiver; +import energy.trolie.client.request.operatingsnapshots.SeasonalSnapshotReceiver; +import energy.trolie.client.request.operatingsnapshots.SeasonalSnapshotSubscribedReceiver; import energy.trolie.client.request.ratingproposals.ForecastRatingProposalUpdate; import energy.trolie.client.request.ratingproposals.RealTimeRatingProposalUpdate; import lombok.extern.slf4j.Slf4j; @@ -1518,6 +1522,265 @@ public void setSubscription(RequestSubscription subscription) { } } + @Test + void testSeasonalSnapshotGet() throws Exception { + + Instant startTime = Instant.now(); + String season = "FALL"; + + requestHandler = request -> { + + BasicClassicHttpResponse response = new BasicClassicHttpResponse(200); + + try { + + //we expect to get the monitoring set name as a query param + Assertions.assertEquals( + TrolieApiConstants.PARAM_MONITORING_SET + "=abc", + request.getUri().getQuery()); + + //on 1st and 3rd request, return a new snapshot to indicate an update + PipedOutputStream out = new PipedOutputStream(); + PipedInputStream in = new PipedInputStream(out); + + response.setEntity( + new GzipCompressingEntity(new InputStreamEntity(in,ContentType.create(TrolieApiConstants.CONTENT_TYPE_SEASONAL_SNAPSHOT)))); + response.addHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); + threadPoolExecutor.submit(new Callable() { + @Override + public Void call() throws Exception { + + try (JsonGenerator json = new JsonFactory(objectMapper).createGenerator(out)) { + + writeSeasonalSnapshot(json, startTime, season); + + return null; + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + }); + + } catch (Exception e) { + e.printStackTrace(); + response.setCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); + } + + return response; + }; + + + HttpClientBuilder builder = HttpClientBuilder.create(); + try (TrolieClient trolieClient = new TrolieClientBuilder(baseUri,builder.build()).build();) { + + AtomicInteger snapshotsReceived = new AtomicInteger(0); + AtomicInteger errorCount = new AtomicInteger(0); + + //subscribe for snapshots and validate they are transmitted correctly + trolieClient.getInUseSeasonalSnapshots(new SeasonalSnapshotReceiver() { + + int numResources; + int numPeriods; + + @Override + public void header(SeasonalSnapshotHeader header) { + Assertions.assertNotNull(header); + + } + + + @Override + public void endSnapshot() { + Assertions.assertEquals(100, numResources); + numResources = 0; + } + + @Override + public void beginSnapshot() { + snapshotsReceived.incrementAndGet(); + } + + @Override + public void error(StreamingGetException t) { + errorCount.incrementAndGet(); + } + + + @Override + public void beginResource(String resourceId) { + numResources++; + } + + + @Override + public void period(SeasonalPeriodSnapshot period) { + + Assertions.assertEquals( season, period.getSeasonName()); + numPeriods++; + } + + + @Override + public void endResource() { + Assertions.assertEquals(24, numPeriods); + numPeriods = 0; + } + }, "abc", null); + + Assertions.assertEquals(1, snapshotsReceived.get()); + Assertions.assertEquals(0, errorCount.get()); + + } + } + + @Test + void testSeasonalSnapshotSubscription() throws Exception { + + //we will run the subscription for fixed number of requests + AtomicInteger requestCounter = new AtomicInteger(0); + Instant startTime = Instant.now(); + String season = "FALL"; + String etag = UUID.randomUUID().toString(); + + requestHandler = request -> { + + BasicClassicHttpResponse response = new BasicClassicHttpResponse(200); + + try { + + //we expect to get the configured monitoring set name as a query param + Assertions.assertEquals("monitoring-set=abc", request.getUri().getQuery()); + + Header requestEtag = request.getHeader(HttpHeaders.IF_NONE_MATCH); + + //2nd+ request should have an etag header + if (requestCounter.get() > 0) { + Assertions.assertNotNull(requestEtag); + Assertions.assertEquals(etag, requestEtag.getValue()); + } + + if (requestCounter.get() == 3) { + + //on 4th request return error to test error propagation to receiver + response.setCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); + + } else if (requestCounter.get() % 2 == 0) { + + //on 1st and 3rd request, return a new snapshot to indicate an update + PipedOutputStream out = new PipedOutputStream(); + PipedInputStream in = new PipedInputStream(out); + + response.setHeader(HttpHeaders.ETAG, etag); + response.setEntity( + new GzipCompressingEntity(new InputStreamEntity(in,ContentType.create(TrolieApiConstants.CONTENT_TYPE_SEASONAL_SNAPSHOT)))); + response.addHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); + threadPoolExecutor.submit(new Callable() { + @Override + public Void call() throws Exception { + + try (JsonGenerator json = new JsonFactory(objectMapper).createGenerator(out)) { + writeSeasonalSnapshot(json, startTime, season ); + return null; + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + }); + + } else { + + //on 2nd request, indicate existing etag is valid to test that request is short-circuited + response.setCode(HttpStatus.SC_NOT_MODIFIED); + } + + } catch (Exception e) { + e.printStackTrace(); + response.setCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); + } finally { + requestCounter.incrementAndGet(); + } + + return response; + }; + + + HttpClientBuilder builder = HttpClientBuilder.create(); + try (TrolieClient trolieClient = new TrolieClientBuilder(baseUri,builder.build()) + .seasonalRatingsPollMs(200) + .build();) { + + AtomicInteger snapshotsReceived = new AtomicInteger(0); + AtomicInteger errorCount = new AtomicInteger(0); + + //subscribe for snapshots and validate they are transmitted correctly + var subscription = trolieClient.subscribeToInUseSeasonalSnapshotUpdates(new SeasonalSnapshotSubscribedReceiver() { + + RequestSubscription subscription; + int numResources; + int numPeriods; + + @Override + public void period(SeasonalPeriodSnapshot period) { + numPeriods++; + Assertions.assertEquals(season, period.getSeasonName()); + + } + + @Override + public void header(SeasonalSnapshotHeader header) { + + Assertions.assertNotNull(header); + } + + @Override + public void endSnapshot() { + Assertions.assertEquals(100, numResources); + numResources = 0; + } + + @Override + public void endResource() { + Assertions.assertEquals(24, numPeriods); + numPeriods = 0; + } + + @Override + public void beginSnapshot() { + snapshotsReceived.incrementAndGet(); + } + + @Override + public void beginResource(String resourceId) { + numResources++; + } + + @Override + public void error(StreamingGetException t) { + errorCount.incrementAndGet(); + ((RequestSubscriptionInternal)subscription).stop(); + } + + @Override + public void setSubscription(RequestSubscription subscription) { + this.subscription = subscription; + } + + + }, "abc"); + int counter = 0; + while (subscription.isSubscribed()) { + Thread.sleep(100); + } + + //we should have received 2 snapshots, 1 304 code and 1 500 code + Assertions.assertEquals(2, snapshotsReceived.get()); + Assertions.assertEquals(1, errorCount.get()); + } + } + private void writeMonitoringSet(JsonGenerator json, String id) throws IOException { var source = DataProvenance.builder().provider(id).lastUpdated( Instant.now()).originId(id).build(); @@ -1594,4 +1857,42 @@ private void writeRealTimeSnapshot(JsonGenerator json) throws IOException { json.writeEndArray(); json.writeEndObject(); } + + private void writeSeasonalSnapshot(JsonGenerator json, Instant startTime, String season) throws IOException { + + SeasonalSnapshotHeader header = new SeasonalSnapshotHeader(); + + json.writeStartObject(); + + json.writeFieldName("snapshot-header"); + json.writeObject(header); + + json.writeFieldName("ratings"); + json.writeStartArray(); + + for (int i=0;i<100;i++) { + json.writeStartObject(); + json.writeFieldName("resource-id"); + json.writeString("resource" + i); + json.writeFieldName("periods"); + json.writeStartArray(); + for (int j=0;j<24;j++) { + SeasonalPeriodSnapshot period = new SeasonalPeriodSnapshot( + startTime, + startTime, + season, + RatingValue.fromMva(100f), + Collections.emptyList() + ); + json.writeObject(period); + } + json.writeEndArray(); + json.writeEndObject(); + } + + json.writeEndArray(); + json.writeEndObject(); + + } + }