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();
+
+ }
+
}