Skip to content

Commit

Permalink
Time-of-Use improvements (#2547)
Browse files Browse the repository at this point in the history
Backported from FEMS 2024.2.2

- Rename State CHARGE to CHARGE_GRID (prepare for future CHARGE_DC_PV, CHARGE_PV,...)
- Extend JSON-RPC Response with Grid Buy/Sell and Ess Charge/Discharge
- UI: extend forecast to show Grid + Ess
- Improve startup time (wait forever till all params are available; instead of trying again in next quarter only)
- Improve calculation of CHARGE_GRID power (C-Rate of usable capacity + consider predicted surplus-consumption)
- Improve RunOptimizerFromLogApp (ignores log header)
- Fix BALANCING reach 100 % SoC
- Use pure BALANCING schedule as additional initial population (makes sure, that this one wins in case there are other results with same cost, e.g. when battery never gets empty anyway)
- Add MetaEvcs -> avoid calculating power twice in Sum UnmanagedConsumption
- Refactorings + Javadoc (clear naming of methods, variables, units, positive/negatie values etc.)
- Improve JUnit integration tests
- Predictors: add logVerbosity configuration setting
- Prediction: replace `Converter` with `ValueRange` - predictions are limited to [0; Max-Ever-Value]

Co-authored-by: Sagar Venu <[email protected]>
  • Loading branch information
sfeilmeier and venu-sagar authored Feb 19, 2024
1 parent ae5b1cd commit 52320a8
Show file tree
Hide file tree
Showing 83 changed files with 2,374 additions and 1,808 deletions.
16 changes: 13 additions & 3 deletions io.openems.common/src/io/openems/common/utils/DateUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.function.BiFunction;

import io.openems.common.exceptions.OpenemsException;
import io.openems.common.timedata.DurationUnit;

public class DateUtils {

Expand All @@ -34,8 +34,18 @@ private DateUtils() {
* @param minutes the minutes to round down to; max 59
* @return the rounded result
*/
public static ZonedDateTime roundZonedDateTimeDownToMinutes(ZonedDateTime d, int minutes) {
return d.withMinute(d.getMinute() - d.getMinute() % minutes).truncatedTo(ChronoUnit.MINUTES);
public static ZonedDateTime roundDownToMinutes(ZonedDateTime d, int minutes) {
return d.truncatedTo(DurationUnit.ofMinutes(minutes));
}

/**
* Rounds a {@link ZonedDateTime} down to next quarter (15 minutes).
*
* @param d the {@link ZonedDateTime}
* @return the rounded result
*/
public static ZonedDateTime roundDownToQuarter(ZonedDateTime d) {
return roundDownToMinutes(d, 15);
}

/**
Expand Down
18 changes: 18 additions & 0 deletions io.openems.common/src/io/openems/common/worker/AbstractWorker.java
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,22 @@ private long onWorkerExceptionSleep(long duration) {
} while (targetTime > System.currentTimeMillis());
return duration;
}

/**
* Changes the priority of this thread.
*
* <p>
* See {@link Thread#setPriority(int)}, {@link Thread#MIN_PRIORITY},
* {@link Thread#NORM_PRIORITY}, {@link Thread#MAX_PRIORITY}}.
*
* @param newPriority priority to set this thread to
* @throws IllegalArgumentException If the priority is not in the range
* {@code MIN_PRIORITY} to
* {@code MAX_PRIORITY}.
* @throws SecurityException if the current thread cannot modify this
* thread.
*/
public final void setPriority(int newPriority) {
this.thread.setPriority(newPriority);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.openems.common.utils;

import static io.openems.common.utils.DateUtils.roundZonedDateTimeDownToMinutes;
import static io.openems.common.utils.DateUtils.roundDownToQuarter;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
Expand All @@ -17,16 +17,14 @@
public class DateUtilsTest {

@Test
public void testRoundZonedDateTimeDownToMinutes() throws Exception {
public void testRoundDownToQuarter() throws Exception {
assertEquals(//
ZonedDateTime.of(2023, 1, 2, 3, 0, 0, 0, ZoneId.of("UTC")), //
roundZonedDateTimeDownToMinutes(//
ZonedDateTime.of(2023, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")), 15));
roundDownToQuarter(ZonedDateTime.of(2023, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC"))));

assertEquals(//
ZonedDateTime.of(2023, 1, 2, 3, 15, 0, 0, ZoneId.of("UTC")), //
roundZonedDateTimeDownToMinutes(//
ZonedDateTime.of(2023, 1, 2, 3, 16, 17, 18, ZoneId.of("UTC")), 15));
roundDownToQuarter(ZonedDateTime.of(2023, 1, 2, 3, 16, 17, 18, ZoneId.of("UTC"))));
}

@Test
Expand Down
66 changes: 66 additions & 0 deletions io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,33 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
.persistencePriority(PersistencePriority.VERY_HIGH) //
.text("Actual AC-side battery discharge power of Energy Storage System. " //
+ "Negative values for charge; positive for discharge")),
/**
* Ess: Minimum Ever Discharge Power (i.e. Maximum Ever Charge power as negative
* value).
*
* <ul>
* <li>Interface: Sum (origin: SymmetricEss))
* <li>Type: Integer
* <li>Unit: W
* <li>Range: negative values or '0'
* </ul>
*/
ESS_MIN_DISCHARGE_POWER(Doc.of(OpenemsType.INTEGER) //
.unit(Unit.WATT) //
.persistencePriority(PersistencePriority.VERY_HIGH)),
/**
* Ess: Maximum Ever Discharge Power.
*
* <ul>
* <li>Interface: Sum (origin: SymmetricEss)
* <li>Type: Integer
* <li>Unit: W
* <li>Range: positive values or '0'
* </ul>
*/
ESS_MAX_DISCHARGE_POWER(Doc.of(OpenemsType.INTEGER) //
.unit(Unit.WATT) //
.persistencePriority(PersistencePriority.VERY_HIGH)),
/**
* Ess: Capacity.
*
Expand Down Expand Up @@ -931,6 +958,45 @@ public default void _setEssDischargePower(int value) {
this.getEssDischargePowerChannel().setNextValue(value);
}

/**
* Gets the Channel for {@link ChannelId#ESS_MAX_DISCHARGE_POWER}.
*
* @return the Channel
*/
public default IntegerReadChannel getEssMaxDischargePowerChannel() {
return this.channel(ChannelId.ESS_MAX_DISCHARGE_POWER);
}

/**
* Gets the Total Maximum Ever ESS Discharge Power in [W]. See
* {@link ChannelId#ESS_MAX_DISCHARGE_POWER}.
*
* @return the Channel {@link Value}
*/
public default Value<Integer> getEssMaxDischargePower() {
return this.getEssMaxDischargePowerChannel().value();
}

/**
* Gets the Channel for {@link ChannelId#ESS_MIN_DISCHARGE_POWER}.
*
* @return the Channel
*/
public default IntegerReadChannel getEssMinDischargePowerChannel() {
return this.channel(ChannelId.ESS_MIN_DISCHARGE_POWER);
}

/**
* Gets the Total Minimum Ever ESS Discharge Power in [W] (i.e. Maximum Ever
* Charge power as negative value). See
* {@link ChannelId#ESS_MIN_DISCHARGE_POWER}.
*
* @return the Channel {@link Value}
*/
public default Value<Integer> getEssMinDischargePower() {
return this.getEssMinDischargePowerChannel().value();
}

/**
* Gets the Channel for {@link ChannelId#ESS_CAPACITY}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.openems.edge.controller.ess.gridoptimizedcharge;

import static io.openems.edge.controller.ess.gridoptimizedcharge.ControllerEssGridOptimizedChargeImpl.DEFAULT_POWER_BUFFER;
import static java.lang.Math.min;
import static java.time.temporal.ChronoField.MINUTE_OF_DAY;

import java.time.Clock;
import java.time.LocalDate;
import java.time.LocalDateTime;
Expand All @@ -25,7 +29,6 @@
import io.openems.edge.common.type.TypeUtils;
import io.openems.edge.ess.power.api.Phase;
import io.openems.edge.ess.power.api.Pwr;
import io.openems.edge.predictor.api.oneday.Prediction24Hours;

public class DelayCharge {

Expand Down Expand Up @@ -91,7 +94,7 @@ protected Integer getManualDelayChargeMaxCharge() throws OpenemsNamedException {
this.parent.logDebug(noValidManualTargetTime.channelDoc().getText());
}

var targetMinute = targetTime.get(ChronoField.MINUTE_OF_DAY);
var targetMinute = targetTime.get(MINUTE_OF_DAY);
return this.calculateDelayChargeMaxCharge(targetMinute, DelayChargeRiskLevel.MEDIUM);
}

Expand Down Expand Up @@ -153,16 +156,16 @@ private Integer getCalculatedTargetMinute() {

// Predictions
var hourlyPredictionProduction = this.parent.predictorManager
.get24HoursPrediction(new ChannelAddress("_sum", "ProductionActivePower"));
.getPrediction(new ChannelAddress("_sum", "ProductionActivePower"));
var hourlyPredictionConsumption = this.parent.predictorManager
.get24HoursPrediction(new ChannelAddress("_sum", "ConsumptionActivePower"));
.getPrediction(new ChannelAddress("_sum", "ConsumptionActivePower"));

var now = ZonedDateTime.now(this.parent.componentManager.getClock());
var predictionStartQuarterHour = roundZonedDateTimeDownTo15Minutes(now);

// Predictions as Integer array
var hourlyProduction = hourlyPredictionProduction.getValues();
var hourlyConsumption = hourlyPredictionConsumption.getValues();
var hourlyProduction = hourlyPredictionProduction.asArray();
var hourlyConsumption = hourlyPredictionConsumption.asArray();

// Displays the production values once, if debug mode is activated.
if (this.predictionDebugLog) {
Expand Down Expand Up @@ -288,14 +291,14 @@ private Integer calculateDelayChargeMaxCharge(Integer targetMinute, DelayChargeR
var remainingTime = DelayCharge.calculateRemainingTime(clock, targetMinute);

// Predictions
Prediction24Hours quarterHourlyPredictionProduction = this.parent.predictorManager
.get24HoursPrediction(new ChannelAddress("_sum", "ProductionActivePower"));
Prediction24Hours quarterHourlyPredictionConsumption = this.parent.predictorManager
.get24HoursPrediction(new ChannelAddress("_sum", "ConsumptionActivePower"));
var quarterHourlyPredictionProduction = this.parent.predictorManager
.getPrediction(new ChannelAddress("_sum", "ProductionActivePower"));
var quarterHourlyPredictionConsumption = this.parent.predictorManager
.getPrediction(new ChannelAddress("_sum", "ConsumptionActivePower"));

// Predictions as Integer array
Integer[] quarterHourlyProduction = quarterHourlyPredictionProduction.getValues();
Integer[] quarterHourlyConsumption = quarterHourlyPredictionConsumption.getValues();
var quarterHourlyProduction = quarterHourlyPredictionProduction.asArray();
var quarterHourlyConsumption = quarterHourlyPredictionConsumption.asArray();

// Max apparent power
int maxApparentPower = this.parent.ess.getMaxApparentPower().getOrError();
Expand All @@ -306,8 +309,7 @@ private Integer calculateDelayChargeMaxCharge(Integer targetMinute, DelayChargeR
var minimumPowerFactor = MINIMUM_POWER_FACTOR;

boolean delayChargeMinimumReached = this.parent.getDelayChargeStateChannel().getPastValues()
.tailMap(LocalDateTime.now(this.parent.componentManager.getClock()).with(ChronoField.MINUTE_OF_DAY, 5),
true)
.tailMap(LocalDateTime.now(this.parent.componentManager.getClock()).with(MINUTE_OF_DAY, 5), true)
.values().stream().filter(Value::isDefined)
.filter(channel -> channel.asEnum() == DelayChargeState.ACTIVE_LIMIT).findAny().isPresent();

Expand Down Expand Up @@ -347,7 +349,7 @@ private Integer calculateDelayChargeMaxCharge(Integer targetMinute, DelayChargeR

// Reduce limit to MaxApparentPower to avoid very high values in the last
// seconds
calculatedPower = Math.min(calculatedPower, maxApparentPower);
calculatedPower = min(calculatedPower, maxApparentPower);

/*
* Calculate the average with the last 900 limits
Expand Down Expand Up @@ -498,30 +500,28 @@ protected static int calculateRemainingTime(Clock clock, int targetMinute) {
protected static Optional<Integer> calculateTargetMinute(Integer[] quarterHourlyProduction,
Integer[] quarterHourlyConsumption, ZonedDateTime predictionStartQuarterHour) {

var predictionStartQuarterHourIndex = predictionStartQuarterHour.get(ChronoField.MINUTE_OF_DAY) / 15;
var predictionStartQuarterHourIndex = predictionStartQuarterHour.get(MINUTE_OF_DAY) / 15;

// Last hour when production was greater than consumption.
Optional<Integer> lastQuarterHour = Optional.empty();

// Iterate predictions till midnight
for (var i = 0; i < 96 - predictionStartQuarterHourIndex; i++) {
for (var i = 0; i < min(96 - predictionStartQuarterHourIndex, quarterHourlyProduction.length); i++) {
// to avoid null and negative consumption values.
if (quarterHourlyProduction[i] != null && quarterHourlyConsumption[i] != null
&& quarterHourlyConsumption[i] >= 0) {

// Updating last quarter hour if production is higher than consumption plus
// power buffer
if (quarterHourlyProduction[i] > quarterHourlyConsumption[i]
+ ControllerEssGridOptimizedChargeImpl.DEFAULT_POWER_BUFFER) {
if (quarterHourlyProduction[i] > quarterHourlyConsumption[i] + DEFAULT_POWER_BUFFER) {
lastQuarterHour = Optional.of(i);
}
}
}
if (lastQuarterHour.isPresent()) {
return Optional.of(
predictionStartQuarterHour.plusMinutes(lastQuarterHour.get() * 15).get(ChronoField.MINUTE_OF_DAY));
}
return Optional.empty();

return lastQuarterHour.map(t -> {
return predictionStartQuarterHour.plusMinutes(t * 15).get(MINUTE_OF_DAY);
});
}

/**
Expand All @@ -545,8 +545,8 @@ protected static int calculateAvailEnergy(Integer[] quarterHourlyProduction, Int
ZonedDateTime now = ZonedDateTime.now(clock);
ZonedDateTime predictionStartQuarterHour = DelayCharge.roundZonedDateTimeDownTo15Minutes(now);

int dailyStartIndex = predictionStartQuarterHour.get(ChronoField.MINUTE_OF_DAY) / 15;
int dailyEndIndex = DelayCharge.getAsZonedDateTime(targetMinute, clock).get(ChronoField.MINUTE_OF_DAY) / 15;
int dailyStartIndex = predictionStartQuarterHour.get(MINUTE_OF_DAY) / 15;
int dailyEndIndex = DelayCharge.getAsZonedDateTime(targetMinute, clock).get(MINUTE_OF_DAY) / 15;

// Relevant quarter hours
int endIndex = dailyEndIndex - dailyStartIndex;
Expand Down Expand Up @@ -594,7 +594,7 @@ protected static int calculateAvailEnergy(Integer[] quarterHourlyProduction, Int
* @return the rounded result
*/
private static ZonedDateTime roundZonedDateTimeDownTo15Minutes(ZonedDateTime d) {
var minuteOfDay = d.get(ChronoField.MINUTE_OF_DAY);
var minuteOfDay = d.get(MINUTE_OF_DAY);
return d.with(ChronoField.NANO_OF_DAY, 0).plus(minuteOfDay / 15 * 15, ChronoUnit.MINUTES);
}

Expand All @@ -605,7 +605,7 @@ private static ZonedDateTime roundZonedDateTimeDownTo15Minutes(ZonedDateTime d)
* @return the rounded result
*/
private static ZonedDateTime roundZonedDateTimeUpTo5Minutes(ZonedDateTime d) {
var minuteOfDay = d.get(ChronoField.MINUTE_OF_DAY);
var minuteOfDay = d.get(MINUTE_OF_DAY);
long roundMinutes = TypeUtils.getAsType(OpenemsType.LONG, Math.ceil(minuteOfDay / 5.0) * 5);
return d.with(ChronoField.NANO_OF_DAY, 0).plusMinutes(roundMinutes);
}
Expand All @@ -630,7 +630,7 @@ protected void setDelayChargeStateAndLimit(DelayChargeState state, Integer limit
*/
private static boolean passedTargetMinute(int targetMinute, Clock clock) {

if (ZonedDateTime.now(clock).get(ChronoField.MINUTE_OF_DAY) >= targetMinute) {
if (ZonedDateTime.now(clock).get(MINUTE_OF_DAY) >= targetMinute) {
return true;
}
return false;
Expand Down
Loading

0 comments on commit 52320a8

Please sign in to comment.