Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
Expand Down Expand Up @@ -77,6 +78,12 @@ public class DistroEnergyHandler {
*/
protected static final int MAX_DAYS_AHEAD = 5;

/**
* Day-ahead gate closure, market time. From this moment tomorrow is closed and the API rejects a
* submission for it, so a run at or after it starts at the day after tomorrow.
*/
protected static final LocalTime GATE_CLOSURE = LocalTime.of(9, 0);

/** The {@link EmsDistroEnergyAsset} this handler reports its status on. */
protected final String assetId;

Expand Down Expand Up @@ -189,19 +196,22 @@ public void deploy() {
}

protected void submitDayAheadForecasts() {
LocalDate firstDay = timerService.getNow().atZone(marketZone).toLocalDate().plusDays(1);
LocalDateTime marketNow = timerService.getNow().atZone(marketZone).toLocalDateTime();
LocalDate today = marketNow.toLocalDate();
LocalDate firstDay = today.plusDays(marketNow.toLocalTime().isBefore(GATE_CLOSURE) ? 1 : 2);
LocalDate lastDay = today.plusDays(MAX_DAYS_AHEAD);
LOG.fine(
"Starting day-ahead submission run for portfolio "
+ portfolio
+ "; first market day "
+ "; market days "
+ firstDay
+ ", horizon up to "
+ MAX_DAYS_AHEAD
+ " day(s)");
+ " to "
+ lastDay);
int submitted = 0;

for (int day = 0; day < MAX_DAYS_AHEAD; day++) {
LocalDate marketDate = firstDay.plusDays(day);
for (LocalDate marketDate = firstDay;
!marketDate.isAfter(lastDay);
marketDate = marketDate.plusDays(1)) {
try {
if (!submitDayAheadForecast(marketDate)) {
// First uncovered day is the end of the forecast horizon; nothing beyond it to send.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ class DistroEnergyHandlerStatusTest extends Specification {

AssetProcessingService assetProcessingService
Container container
long now = NOW
FixedHorizonHandler handler

def setup() {
assetProcessingService = Mock(AssetProcessingService)
def timerService = Stub(TimerService) {
getCurrentTimeMillis() >> NOW
getNow() >> Instant.ofEpochMilli(NOW)
getCurrentTimeMillis() >> { now }
getNow() >> { Instant.ofEpochMilli(now) }
}
container = Stub(Container) {
getConfig() >> [(DistroEnergyHandler.DISTRO_ENERGY_CLIENT_KEY): "test-key"]
Expand Down Expand Up @@ -96,10 +97,29 @@ class DistroEnergyHandlerStatusTest extends Specification {
0 * assetProcessingService._
}

def "a run before gate closure starts at tomorrow, from gate closure at the day after"() {
given: "a handler whose forecast covers every day"
now = Instant.parse(instant).toEpochMilli()
handler = new FixedHorizonHandler(Integer.MAX_VALUE, container)

when:
handler.submitDayAheadForecasts()

then: "the run ends five days after today, market time, either way"
handler.marketDates.first() == LocalDate.parse(firstDay)
handler.marketDates.last() == LocalDate.parse("2026-10-04")

where: "09:00 Europe/Amsterdam is 07:00Z during CEST"
instant | firstDay
"2026-09-29T06:59:59Z" | "2026-09-30"
"2026-09-29T07:00:00Z" | "2026-10-01"
"2026-09-29T21:59:59Z" | "2026-10-01"
}

// Reports a forecast covering exactly `horizonDays` market days without touching the API.
static class FixedHorizonHandler extends DistroEnergyHandler {
final int horizonDays
int calls = 0
final List<LocalDate> marketDates = []

FixedHorizonHandler(int horizonDays, Container container) {
super(ASSET_ID, new AttributeRef("parent", "powerNet"), "portfolio-a", container)
Expand All @@ -108,7 +128,8 @@ class DistroEnergyHandlerStatusTest extends Specification {

@Override
protected boolean submitDayAheadForecast(LocalDate marketDate) {
return calls++ <horizonDays
marketDates << marketDate
return marketDates.size() <= horizonDays
}
}
}
Loading