+ * positive = discharge; negative = charge + *
+ * + * @param gridPower the active power of the meter [W] + * @param essPower the active power of the ess [W] + * @param pucSocWs the soc of the puc [Ws] + * @param essCapacityWs the ess capacity [Ws] + * @param minEssPower the minimum possible power of the ess [W] + * @param maxEssPower the maximum possible power of the ess [W] + * @param efficiency the efficiency of the system [%] + * @param cycleTimeS the configured openems cycle time [seconds] + * + * @return the puc battery power for the next cycle [W] + */ + protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocWs, long essCapacityWs, + int minEssPower, int maxEssPower, double efficiency, double cycleTimeS) { + this.log.debug("### calculatePucBatteryPower ###"); + this.log.debug(String.format( + "Parameters: gridPower=%d, essPower=%d, pucSocWs=%d, essCapacityWs=%d, minEssPower=%d, " + + "maxEssPower=%d, efficiency=%.2f, cycleTimeS=%.2f", + gridPower, essPower, pucSocWs, essCapacityWs, minEssPower, maxEssPower, efficiency, cycleTimeS)); + // calculate pucPower without any limits + var pucBatteryPower = gridPower + essPower; + this.log.debug("pucBatteryPower without limits: " + pucBatteryPower); + + // apply ess power limits + pucBatteryPower = TypeUtils.fitWithin(minEssPower, maxEssPower, pucBatteryPower); + this.log.debug("pucBatteryPower with ess power limits: " + pucBatteryPower); + + // apply soc bounds + pucBatteryPower = this.applyPucSocBounds(pucBatteryPower, pucSocWs, essCapacityWs, efficiency, cycleTimeS); + this.log.debug("pucBatteryPower with ess power and soc limits: " + pucBatteryPower); + return pucBatteryPower; + } + + /** + * Checks and corrects the puc battery power if it would exceed the upper or + * lower limits of the soc. + * + * @param pucPower the calculated pucPower [W] + * @param pucSocWs the soc of the puc [Ws] + * @param essCapacityWs the ess capacity [Ws] + * @param efficiency the efficiency of the system [%] + * @param cycleTimeS the configured openems cycle time [seconds] + * @return the restricted pucPower [W] + */ + protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, double efficiency, + double cycleTimeS) { + var dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; + var dischargeEnergyUpperBoundWs = pucSocWs; + + var powerLowerBound = Efficiency.unapply(round(dischargeEnergyLowerBoundWs / cycleTimeS), efficiency); + var powerUpperBound = Efficiency.unapply(round(dischargeEnergyUpperBoundWs / cycleTimeS), efficiency); + + if (powerLowerBound > 0) { + powerLowerBound = 0; + } + if (powerUpperBound < 0) { + powerUpperBound = 0; + } + + return (int) TypeUtils.fitWithin(powerLowerBound, powerUpperBound, pucPower); + } + + /** + * Calculates the battery power for the levl use case considering various + * constraints. + *+ * positive = discharge; negative = charge + *
+ * + * @param remainingLevlEnergyWs the remaining energy that has to be realized + * for levl [Ws] + * @param pucBatteryPower the puc battery power [W] + * @param minEssPower the minimum possible power of the ess [W] + * @param maxEssPower the maximum possible power of the ess [W] + * @param pucGridPower the active power of the puc on the meter [W] + * @param buyFromGridLimit maximum power that may be bought from the + * grid [W] + * @param sellToGridLimit maximum power that may be sold to the grid + * [W] + * @param nextPucSocWs the calculated puc soc for the next cycle + * [Ws] + * @param levlSocWs the current levl soc [Ws] + * @param socLowerBoundLevlPercent the lower levl soc limit [%] + * @param socUpperBoundLevlPercent the upper levl soc limit [%] + * @param essCapacityWs the ess capacity [Ws] + * @param influenceSellToGrid whether it's allowed to influence sell to + * grid + * @param efficiency the efficiency of the system [%] + * @param cycleTimeS the configured openems cycle time [seconds] + * @return the levl battery power [W] + */ + protected int calculateLevlBatteryPower(long remainingLevlEnergyWs, int pucBatteryPower, int minEssPower, + int maxEssPower, int pucGridPower, long buyFromGridLimit, long sellToGridLimit, long nextPucSocWs, + long levlSocWs, double socLowerBoundLevlPercent, double socUpperBoundLevlPercent, long essCapacityWs, + boolean influenceSellToGrid, double efficiency, double cycleTimeS) { + + this.log.debug("### calculateLevlBatteryPowerW ###"); + this.log.debug(String.format( + "Parameters: remainingLevlEnergyWs=%d, pucBatteryPower=%d, minEssPower=%d, maxEssPower=%d, " + + "pucGridPower=%d, buyFromGridLimit=%d, sellToGridLimit=%d, nextPucSocWs=%d, levlSocWs=%d, " + + "socLowerBoundLevlPercent=%.2f, socUpperBoundLevlPercent=%.2f, essCapacityWs=%d, " + + "influenceSellToGrid=%b, efficiency=%.2f, cycleTimeS=%.2f", + remainingLevlEnergyWs, pucBatteryPower, minEssPower, maxEssPower, pucGridPower, buyFromGridLimit, + sellToGridLimit, nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, socUpperBoundLevlPercent, + essCapacityWs, influenceSellToGrid, efficiency, cycleTimeS)); + + var levlPower = round(remainingLevlEnergyWs / (double) cycleTimeS); + this.log.debug("Initial levlPower: " + levlPower); + + levlPower = this.applyBatteryPowerLimitsToLevlPower(levlPower, pucBatteryPower, minEssPower, maxEssPower); + this.log.debug("LevlPower after applyBatteryPowerLimits: " + levlPower); + + levlPower = this.applySocBoundariesToLevlPower(levlPower, nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, + socUpperBoundLevlPercent, essCapacityWs, efficiency, cycleTimeS); + this.log.debug("LevlPower after applySocBoundaries: " + levlPower); + + levlPower = this.applyGridPowerLimitsToLevlPower(levlPower, pucGridPower, buyFromGridLimit, sellToGridLimit); + this.log.debug("LevlPower after applyGridPowerLimits: " + levlPower); + + levlPower = this.applyInfluenceSellToGridConstraint(levlPower, pucGridPower, influenceSellToGrid); + this.log.debug("LevlPower after applyInfluenceSellToGridConstraint: " + levlPower); + + return (int) levlPower; + } + + /** + * Applies battery power limits to the levl power. + * + * @param levlPower the levl battery power [W] + * @param pucBatteryPower the puc battery power [W] + * @param minEssPower the minimum possible power of the ess [W] + * @param maxEssPower the maximum possible power of the ess [W] + * @return the restricted levl battery power [W] + */ + protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, + int maxEssPower) { + var levlPowerLowerBound = Long.valueOf(minEssPower) - pucBatteryPower; + var levlPowerUpperBound = Long.valueOf(maxEssPower) - pucBatteryPower; + return TypeUtils.fitWithin(levlPowerLowerBound, levlPowerUpperBound, levlPower); + } + + /** + * Applies upper and lower soc bounderies to the levl power. + * + * @param levlPower the levl battery power [W] + * @param nextPucSocWs the calculated puc soc for the next cycle + * [Ws] + * @param levlSocWs the current levl soc [Ws] + * @param socLowerBoundLevlPercent the lower levl soc limit [%] + * @param socUpperBoundLevlPercent the upper levl soc limit [%] + * @param essCapacityWs the ess capacity [Ws] + * @param efficiency the efficiency of the system [%] + * @param cycleTimeS the configured openems cycle time [seconds] + * @return the restricted levl battery power [W] + */ + protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long levlSocWs, + double socLowerBoundLevlPercent, double socUpperBoundLevlPercent, long essCapacityWs, double efficiency, + double cycleTimeS) { + var levlSocLowerBoundWs = round(socLowerBoundLevlPercent / HUNDRED_PERCENT * essCapacityWs) - nextPucSocWs; + var levlSocUpperBoundWs = round(socUpperBoundLevlPercent / HUNDRED_PERCENT * essCapacityWs) - nextPucSocWs; + + if (levlSocLowerBoundWs > 0) { + levlSocLowerBoundWs = 0; + } + if (levlSocUpperBoundWs < 0) { + levlSocUpperBoundWs = 0; + } + + var levlDischargeEnergyLowerBoundWs = -(levlSocUpperBoundWs - levlSocWs); + var levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - levlSocWs); + + var levlPowerLowerBound = Efficiency.unapply(round(levlDischargeEnergyLowerBoundWs / cycleTimeS), efficiency); + var levlPowerUpperBound = Efficiency.unapply(round(levlDischargeEnergyUpperBoundWs / cycleTimeS), efficiency); + + return TypeUtils.fitWithin(levlPowerLowerBound, levlPowerUpperBound, levlPower); + } + + /** + * Applies grid power limits to the levl power. + * + * @param levlPower the levl battery power [W] + * @param pucGridPower the active power of the puc on the meter [W] + * @param buyFromGridLimit maximum power that may be bought from the grid [W] + * @param sellToGridLimit maximum power that may be sold to the grid [W] + * @return the restricted levl battery power [W] + */ + protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, long buyFromGridLimit, + long sellToGridLimit) { + var levlPowerLowerBound = -(buyFromGridLimit - pucGridPower); + var levlPowerUpperBound = -(sellToGridLimit - pucGridPower); + return TypeUtils.fitWithin(levlPowerLowerBound, levlPowerUpperBound, levlPower); + } + + /** + * Applies influence sell to grid constraint to the levl power. + * + * @param levlPower the levl battery power [W] + * @param pucGridPower the active power of the puc on the meter [W] + * @param influenceSellToGrid whether it's allowed to influence sell to grid + * @return the restricted levl battery power [W] + */ + protected long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower, boolean influenceSellToGrid) { + if (!influenceSellToGrid) { + if (pucGridPower < 0) { + // if primary use case sells to grid, levl isn't allowed to do anything + levlPower = 0; + } else { + // if primary use case buys from grid, levl can sell maximum this amount to grid + levlPower = min(levlPower, pucGridPower); + } + } + return levlPower; + } + + @Override + public void buildJsonApiRoutes(JsonApiBuilder builder) { + builder.handleRequest(METHOD, call -> { + return this.handleRequest(call); + }); + } + + /** + * Handles an incoming levl request. Updates the levl soc based on the request + * levl soc. + * + * @param call the JSON-RPC call + * @return a JSON-RPC response + * @throws OpenemsNamedException on error + */ + protected JsonrpcResponse handleRequest(Call+ * Negative values for charge; positive for discharge + *
+ * + * @param value power/energy to which the efficiency should be + * applied + * @param efficiencyPercent efficiency which should be applied + * @return the power/energy inside the battery after applying the efficiency + */ + public static long apply(long value, double efficiencyPercent) { + if (value <= 0) { // charge + return multiplyByEfficiency(value, efficiencyPercent); + } + + // discharge + return divideByEfficiency(value, efficiencyPercent); + } + + /** + * Unapplies an efficiency to a power/energy inside of the battery. + * + *+ * negative values for charge; positive for discharge + *
+ * + * @param value power/energy to which the efficiency should be + * unapplied + * @param efficiencyPercent efficiency which should be unapplied + * @return the power/energy outside the battery after unapplying the efficiency + */ + public static long unapply(long value, double efficiencyPercent) { + if (value <= 0) { // charge + return divideByEfficiency(value, efficiencyPercent); + } + + // discharge + return multiplyByEfficiency(value, efficiencyPercent); + } + + private static long divideByEfficiency(long value, double efficiencyPercent) { + return Math.round(value / (efficiencyPercent / 100)); + } + + private static long multiplyByEfficiency(long value, double efficiencyPercent) { + return Math.round(value * efficiencyPercent / 100); + } +} \ No newline at end of file diff --git a/io.openems.edge.levl.controller/test/.gitignore b/io.openems.edge.levl.controller/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/BalancingImplTest.java b/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/BalancingImplTest.java new file mode 100644 index 00000000000..d533a76e566 --- /dev/null +++ b/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/BalancingImplTest.java @@ -0,0 +1,956 @@ +package io.openems.edge.levl.controller; + +import static io.openems.edge.common.test.TestUtils.createDummyClock; + +import java.time.Instant; + +import org.junit.Test; + +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.common.test.DummyCycle; +import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.ess.test.DummyPower; +import io.openems.edge.meter.test.DummyElectricityMeter; + +public class BalancingImplTest { + + private static final String CTRL_ID = "ctrl0"; + + private static final String ESS_ID = "ess0"; + private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "ActivePower"); + private static final ChannelAddress ESS_SOC = new ChannelAddress(ESS_ID, "Soc"); + private static final ChannelAddress ESS_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, + "SetActivePowerEquals"); + private static final ChannelAddress ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID = new ChannelAddress(ESS_ID, + "SetActivePowerEqualsWithPid"); + private static final ChannelAddress DEBUG_SET_ACTIVE_POWER = new ChannelAddress(ESS_ID, "DebugSetActivePower"); + + private static final String METER_ID = "meter0"; + private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); + private static final ChannelAddress LEVL_REMAINING_LEVL_ENERGY = new ChannelAddress(CTRL_ID, "RemainingLevlEnergy"); + private static final ChannelAddress LEVL_SOC = new ChannelAddress(CTRL_ID, "LevlStateOfCharge"); + private static final ChannelAddress LEVL_SELL_TO_GRID_LIMIT = new ChannelAddress(CTRL_ID, "SellToGridLimit"); + private static final ChannelAddress LEVL_BUY_FROM_GRID_LIMIT = new ChannelAddress(CTRL_ID, "BuyFromGridLimit"); + private static final ChannelAddress SOC_LOWER_BOUND_LEVL = new ChannelAddress(CTRL_ID, + "StateOfChargeLowerBoundLevl"); + private static final ChannelAddress SOC_UPPER_BOUND_LEVL = new ChannelAddress(CTRL_ID, + "StateOfChargeUpperBoundLevl"); + private static final ChannelAddress LEVL_INFLUENCE_SELL_TO_GRID = new ChannelAddress(CTRL_ID, + "InfluenceSellToGrid"); + private static final ChannelAddress LEVL_EFFICIENCY = new ChannelAddress(CTRL_ID, "EssEfficiency"); + private static final ChannelAddress PUC_BATTERY_POWER = new ChannelAddress(CTRL_ID, "PrimaryUseCaseBatteryPower"); + + @Test + public void testWithoutLevlRequest() throws Exception { + final var clock = createDummyClock(); + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withSoc(50) // 900.000.000 Ws + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 20000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 20000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 6000)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 20000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 12000)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 3793) // + .input(METER_ACTIVE_POWER, 20000 - 3793) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 16483)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 8981) // + .input(METER_ACTIVE_POWER, 20000 - 8981) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 19649)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 13723) // + .input(METER_ACTIVE_POWER, 20000 - 13723) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 21577)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 17469) // + .input(METER_ACTIVE_POWER, 20000 - 17469) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 22436)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 20066) // + .input(METER_ACTIVE_POWER, 20000 - 20066) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 22531)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 21564) // + .input(METER_ACTIVE_POWER, 20000 - 21564) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 22171)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 22175) // + .input(METER_ACTIVE_POWER, 20000 - 22175) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 21608)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 22173) // + .input(METER_ACTIVE_POWER, 20000 - 22173) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 21017)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 21816) // + .input(METER_ACTIVE_POWER, 20000 - 21816) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 20508)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 21311) // + .input(METER_ACTIVE_POWER, 20000 - 21311) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 20129)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 20803) // + .input(METER_ACTIVE_POWER, 20000 - 20803) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 19889)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 20377) // + .input(METER_ACTIVE_POWER, 20000 - 20377) // + .output(ESS_SET_ACTIVE_POWER_EQUALS, 19767)); // + } + + @Test + public void testWithLevlDischargeRequest() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withSoc(50) // 900.000.000 Ws + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + // following values have to be initialized in the first cycle + .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) // + .input(SOC_LOWER_BOUND_LEVL, 0) // + .input(SOC_UPPER_BOUND_LEVL, 100) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) // + .input(LEVL_EFFICIENCY, 80.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, 10000) // + .input(LEVL_SOC, 100_000) // + // following values have to be updated each cycle + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 20000) // + .input(DEBUG_SET_ACTIVE_POWER, 30000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 30000) // + .output(PUC_BATTERY_POWER, 20000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 0L) // + .output(LEVL_SOC, 87500L)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 30000) // + .input(METER_ACTIVE_POWER, -10000) // + .input(DEBUG_SET_ACTIVE_POWER, 20000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 20000) // + .output(PUC_BATTERY_POWER, 20000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 0L) // + .output(LEVL_SOC, 87500L)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 20000) // + .input(METER_ACTIVE_POWER, 0) // + .input(DEBUG_SET_ACTIVE_POWER, 20000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 20000) // + .output(PUC_BATTERY_POWER, 20000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 0L) // + .output(LEVL_SOC, 87500L)); // + } + + @Test + public void testWithLevlChargeRequest() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withSoc(50) // 900.000.000 Ws + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + // following values have to be initialized in the first cycle + .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) // + .input(SOC_LOWER_BOUND_LEVL, 0) // + .input(SOC_UPPER_BOUND_LEVL, 100) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) // + .input(LEVL_EFFICIENCY, 80.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, -10000) // + .input(LEVL_SOC, 100_000) // + // following values have to be updated each cycle + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 20000) // + .input(DEBUG_SET_ACTIVE_POWER, 10000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 10000) // + .output(PUC_BATTERY_POWER, 20000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 0L) // + .output(LEVL_SOC, 108000L)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 10000) // + .input(METER_ACTIVE_POWER, 10000) // + .input(DEBUG_SET_ACTIVE_POWER, 20000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 20000) // + .output(PUC_BATTERY_POWER, 20000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 0L) // + .output(LEVL_SOC, 108000L)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 20000) // + .input(METER_ACTIVE_POWER, 0) // + .input(DEBUG_SET_ACTIVE_POWER, 20000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 20000) // + .output(PUC_BATTERY_POWER, 20000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 0L) // + .output(LEVL_SOC, 108000L)); // + } + + // Test with discharge request (ws) > MAX_INT. Constrained by sell to grid + // limit. + @Test + public void testWithLargeLevlDischargeRequest() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withSoc(50) // 900.000.000 Ws + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + // following values have to be initialized in the first cycle + .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) // + .input(SOC_LOWER_BOUND_LEVL, 0) // + .input(SOC_UPPER_BOUND_LEVL, 100) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) // + .input(LEVL_EFFICIENCY, 80.0) // + .input(LEVL_SOC, 100_000) // + .input(LEVL_REMAINING_LEVL_ENERGY, 2_500_000_000L) // + // following values have to be updated each cycle + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 20000) // + .input(DEBUG_SET_ACTIVE_POWER, 120000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 120000) // + .output(PUC_BATTERY_POWER, 20000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 2_499_900_000L) // + .output(LEVL_SOC, -25000L)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 120000) // + .input(METER_ACTIVE_POWER, -100000) // + .input(DEBUG_SET_ACTIVE_POWER, 120000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 120000) // + .output(PUC_BATTERY_POWER, 20000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 2_499_800_000L) // + .output(LEVL_SOC, -150000L)) // + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 120000) // + .input(METER_ACTIVE_POWER, -100000) // + .input(DEBUG_SET_ACTIVE_POWER, 120000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 120000) // + .output(PUC_BATTERY_POWER, 20000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 2_499_700_000L) // + .output(LEVL_SOC, -275000L)); // + } + + @Test + public void testWithReservedChargeCapacityLevlChargesPucMustNotCharge() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + // following values have to be initialized in the first cycle + .input(LEVL_SELL_TO_GRID_LIMIT, -800000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 800000) // + .input(SOC_LOWER_BOUND_LEVL, 0) // + .input(SOC_UPPER_BOUND_LEVL, 100) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) // + .input(LEVL_EFFICIENCY, 80.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) // + .input(LEVL_SOC, -180_000_000) // 10% of total capacity + // following values have to be updated each cycle + .input(ESS_SOC, 90) // 90% = 450,000 Wh = 1,620,000.000 Ws + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, -20_000) // grid power w/o Levl --> sell to grid + .input(DEBUG_SET_ACTIVE_POWER, -500_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // + // puc should not do anything because capacity is completely reserved for Levl + .output(PUC_BATTERY_POWER, 0L) // + // 500,000 Ws should be realized, therefore 9,500,000 Ws are remaining + .output(LEVL_REMAINING_LEVL_ENERGY, -9_500_000L) // + // Levl soc increases by 500,000 Ws * 80% efficiency = 400,000 Ws + .output(LEVL_SOC, -179_600_000L)) // + .next(new TestCase() // + // 90% = 450,000 Wh = 1,620,000,000 Ws | should be 1,620,400,000 Ws but only + // full percent values can be read + .input(ESS_SOC, 90).input(ESS_ACTIVE_POWER, -500_000) // + .input(METER_ACTIVE_POWER, 480_000) // grid power ceteris paribus w/ Levl + .input(DEBUG_SET_ACTIVE_POWER, -500_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // + // since reserved capacity decreased by 400,000 Ws in the previous cycle but ess + // soc value remains the same, puc can charge again + .output(PUC_BATTERY_POWER, -20_000L) // + // 480,000 Ws can be realized for Levl, therefore 9,020,00 are remaining + .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // + // Levl soc increases by 480,000 Ws * 80% efficiency = 384,000 Ws + .output(LEVL_SOC, -179_216_000L)); // + } + + @Test + public void testWithReservedChargeCapacityLevlDischargesPucMustNotCharge() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500_000) // 1,800,000,000 Ws + .withMaxApparentPower(500_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + // following values have to be initialized in the first cycle + .input(LEVL_SELL_TO_GRID_LIMIT, -800_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 800_000) // + .input(SOC_LOWER_BOUND_LEVL, 0) // + .input(SOC_UPPER_BOUND_LEVL, 100) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) // + .input(LEVL_EFFICIENCY, 80.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, 10_000_000) // + .input(LEVL_SOC, -180_000_000) // 10% of total capacity + // following values have to be updated each cycle + .input(ESS_SOC, 90) // 90% = 450,000 Wh = 1,620,000,000 Ws + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, -20_000) // grid power w/o Levl --> sell to grid + .input(DEBUG_SET_ACTIVE_POWER, 500_000) // max discharge power of 500,000 W should be applied + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 500_000) // + // puc should not do anything because capacity is completely reserved for Levl + .output(PUC_BATTERY_POWER, 0L) // + // 500,000 Ws should be realized, therefore 9,500,000 Ws are remaining + .output(LEVL_REMAINING_LEVL_ENERGY, 9_500_000L) // + // Levl soc decreases by 500,000 Ws / 80% efficiency = 625,000 Ws + .output(LEVL_SOC, -180_625_000L)) // + .next(new TestCase() // + // 90% = 450,000 Wh = 1,620,000,000 Ws | should be 1,619,375,000 Ws but only + // full percent values can be read + .input(ESS_SOC, 90) // + .input(ESS_ACTIVE_POWER, 500_000) // + .input(METER_ACTIVE_POWER, -520_000) // grid power ceteris paribus w/ Levl + .input(DEBUG_SET_ACTIVE_POWER, 500_000) // max discharge power of 500,000 W should be applied + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 500_000) // + // puc should not do anything because capacity is still completely reserved for + // Levl + .output(PUC_BATTERY_POWER, 0L) // + // 500,000 Ws should be realized, therefore 9,000,000 Ws are remaining + .output(LEVL_REMAINING_LEVL_ENERGY, 9_000_000L) // + // Levl soc decreases by 500,000 Ws / 80% efficiency = 625,000 Ws + .output(LEVL_SOC, -181_250_000L)); // + } + + @Test + public void testWithReservedChargeCapacityLevlChargesPucMayCharge() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500_000) // 1,800,000,000 Ws + .withMaxApparentPower(500_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + // following values have to be initialized in the first cycle + .input(LEVL_SELL_TO_GRID_LIMIT, -800_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 800_000) // + .input(SOC_LOWER_BOUND_LEVL, 0) // + .input(SOC_UPPER_BOUND_LEVL, 100) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) // + .input(LEVL_EFFICIENCY, 80.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) // + .input(LEVL_SOC, -180_000_000) // 10% of total capacity + // following values have to be updated each cycle + .input(ESS_SOC, 85) // 85% = 425,000 Wh = 1,530,000,000 Ws + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, -10_000) // grid power w/o Levl --> sell to grid + .input(DEBUG_SET_ACTIVE_POWER, -500_000) // max charge power of 500,000 W should be applied + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // + // puc should charge 10,000 Ws since 5% capacity is available + .output(PUC_BATTERY_POWER, -10_000L) // + // 490,000 Ws should be realized, therefore 9,510,000 Ws are remaining + .output(LEVL_REMAINING_LEVL_ENERGY, -9_510_000L) // + // Levl soc increases by 490,000 Ws * 80% efficiency = 392,000 Ws + .output(LEVL_SOC, -179_608_000L)) // + .next(new TestCase() // + // 85% = 425,000 Wh = 1,530,000,000 Ws | should be 1,530,400,000 Ws but only + // full percent values can be read + .input(ESS_SOC, 85) // + .input(ESS_ACTIVE_POWER, -500_000) // + .input(METER_ACTIVE_POWER, 490_000) // grid power ceteris paribus w/ Levl + .input(DEBUG_SET_ACTIVE_POWER, -500_000) // max charge power of 500,000 W should be applied + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // + .output(PUC_BATTERY_POWER, -10_000L) // puc should still charge 10,000 Ws + // 490,000 Ws should be realized, therefore 9,020,000 Ws are remaining + .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // + // Levl soc increases by 490,000 Ws * 80% efficiency = 392,000 Ws + .output(LEVL_SOC, -179_216_000L)); // + } + + @Test + public void testWithReservedChargeCapacityLevlChargesPucMayDischarge() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500_000) // 1,800,000,000 Ws + .withMaxApparentPower(500_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + // following values have to be initialized in the first cycle + .input(LEVL_SELL_TO_GRID_LIMIT, -800_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 800_000) // + .input(SOC_LOWER_BOUND_LEVL, 0) // + .input(SOC_UPPER_BOUND_LEVL, 100) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) // + .input(LEVL_EFFICIENCY, 80.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) // + .input(LEVL_SOC, -180_000_000) // 10% of total capacity + // following values have to be updated each cycle + .input(ESS_SOC, 50) // 50% = 250,000 Wh = 900,000,000 Ws + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 10_000) // grid power w/o Levl --> buy from grid + .input(DEBUG_SET_ACTIVE_POWER, -500_000) // max charge power of 500,000 W should be applied + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // + // puc should discharge 10,000 Ws since Levl has reserved charge not discharge + // energy + .output(PUC_BATTERY_POWER, 10_000L) // + // 510,000 Ws can be realized, because puc discharges 10,000 Ws + .output(LEVL_REMAINING_LEVL_ENERGY, -9_490_000L) // + // Levl soc increases by 510,000 Ws * 80% efficiency = 408,000 Ws + .output(LEVL_SOC, -179_592_000L)) + .next(new TestCase() // + // 50% = 250,000 Wh = 900,000,000 Ws | should be 900,400,000 Ws but only full + // percent values can be read + .input(ESS_SOC, 50) // + .input(ESS_ACTIVE_POWER, -500_000) // + .input(METER_ACTIVE_POWER, 510_000) // grid power ceteris paribus w/ Levl + .input(DEBUG_SET_ACTIVE_POWER, -500_000) // max charge power of 500,000 W should be applied + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // + .output(PUC_BATTERY_POWER, 10_000L) // puc should still charge 10,000 Ws + // 510,000 Ws can be realized again, therefore 8,980,000 Ws are remaining + .output(LEVL_REMAINING_LEVL_ENERGY, -8_980_000L) // + // Levl soc increases by 510,000 Ws * 80% efficiency = 408,000 Ws + .output(LEVL_SOC, -179_184_000L)); // + } + + @Test + public void testInfluenceSellToGrid_PucSellToGrid_LevlChargeForbidden() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase("puc sell to grid, levl charge not allowed") // + .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) // + .input(SOC_LOWER_BOUND_LEVL, 0) // + .input(SOC_UPPER_BOUND_LEVL, 100) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, false) // + .input(LEVL_EFFICIENCY, 80.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, -10000L) // + .input(LEVL_SOC, -180_000_000) // + .input(ESS_SOC, 90) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, -20000) // + .input(DEBUG_SET_ACTIVE_POWER, 0) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) // + .output(PUC_BATTERY_POWER, 0L) // + .output(LEVL_REMAINING_LEVL_ENERGY, -10000L) // + .output(LEVL_SOC, -180_000_000L)); // + } + + @Test + public void testInfluenceSellToGrid_PucSellToGrid_LevlDischargeForbidden() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase("puc sell to grid, levl discharge not allowed") // + .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) // + .input(SOC_LOWER_BOUND_LEVL, 0) // + .input(SOC_UPPER_BOUND_LEVL, 100) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, false) // + .input(LEVL_EFFICIENCY, 80.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, 10000L) // + .input(LEVL_SOC, -180_000_000) // + .input(ESS_SOC, 90) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, -20000) // + .input(DEBUG_SET_ACTIVE_POWER, 0) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) // + .output(PUC_BATTERY_POWER, 0L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 10000L) // + .output(LEVL_SOC, -180_000_000L)); // + } + + @Test + public void testInfluenceSellToGrid_PucBuyFromGrid_LevlChargeAllowed() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase("puc buy from grid, levl charge is allowed") // + .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) // + .input(SOC_LOWER_BOUND_LEVL, 0) // + .input(SOC_UPPER_BOUND_LEVL, 100) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, false) // + .input(LEVL_EFFICIENCY, 80.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, -30000L) // + .input(LEVL_SOC, 0) // + .input(ESS_SOC, 0) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 20000) // + .input(DEBUG_SET_ACTIVE_POWER, -30000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -30000) // + .output(PUC_BATTERY_POWER, 0L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 0L) // + .output(LEVL_SOC, 24000L)); // + } + + @Test + public void testInfluenceSellToGrid_PucBuyFromGrid_LevlDischargeLimited() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase("puc buy from grid, levl discharge is limited to grid limit 0") // + .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) // + .input(SOC_LOWER_BOUND_LEVL, 0) // + .input(SOC_UPPER_BOUND_LEVL, 100) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, false) // + .input(LEVL_EFFICIENCY, 100.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, 30000L) // + .input(LEVL_SOC, 180_000_000L) // + .input(ESS_SOC, 10) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 20000) // + .input(DEBUG_SET_ACTIVE_POWER, 20000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 20000) // + .output(PUC_BATTERY_POWER, 0L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 10000L) // + .output(LEVL_SOC, 179_980_000L)); // + } + + @Test + public void testUpperSocLimit() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withMaxApparentPower(20_000_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + // following values have to be initialized in the first cycle + .input(LEVL_SELL_TO_GRID_LIMIT, -40_000_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 40_000_000) // + .input(SOC_LOWER_BOUND_LEVL, 20) // + .input(SOC_UPPER_BOUND_LEVL, 80) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) // + .input(LEVL_EFFICIENCY, 100.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, -100_000_000) // + .input(LEVL_SOC, 0) // + // following values have to be updated each cycle + .input(ESS_SOC, 79) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 0) // + .input(DEBUG_SET_ACTIVE_POWER, -18_000_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -18_000_000) // + .output(PUC_BATTERY_POWER, -0L) // + .output(LEVL_REMAINING_LEVL_ENERGY, -82_000_000L) // + .output(LEVL_SOC, 18_000_000L)) // + .next(new TestCase() // + .input(ESS_SOC, 80) // + .input(ESS_ACTIVE_POWER, -18_000_000) // + .input(METER_ACTIVE_POWER, 18_000_000) // + .input(DEBUG_SET_ACTIVE_POWER, 0) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) // + .output(PUC_BATTERY_POWER, 0L) // + .output(LEVL_REMAINING_LEVL_ENERGY, -82_000_000L) // + .output(LEVL_SOC, 18_000_000L)) // + .next(new TestCase() // + .input(ESS_SOC, 80) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 0) // + .input(DEBUG_SET_ACTIVE_POWER, 0) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) // + .output(PUC_BATTERY_POWER, 0L) // + .output(LEVL_REMAINING_LEVL_ENERGY, -82_000_000L) // + .output(LEVL_SOC, 18_000_000L)); // + } + + @Test + public void testUpperSocLimit_levlHasCharged() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withMaxApparentPower(30_000_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + // following values have to be initialized in the first cycle + .input(LEVL_SELL_TO_GRID_LIMIT, -40_000_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 40_000_000) // + .input(SOC_LOWER_BOUND_LEVL, 5) // + .input(SOC_UPPER_BOUND_LEVL, 95) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) // + .input(LEVL_EFFICIENCY, 100.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, -100_000_000) // + .input(LEVL_SOC, 36_000_000) // 2% + // following values have to be updated each cycle + .input(ESS_SOC, 94) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, -18_000_000) // + .input(DEBUG_SET_ACTIVE_POWER, -18_000_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -18_000_000) // + .output(PUC_BATTERY_POWER, -18_000_000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, -100_000_000L) // + .output(LEVL_SOC, 36_000_000L)) // + .next(new TestCase() // + .input(ESS_SOC, 95) // + .input(ESS_ACTIVE_POWER, -18_000_000) // + .input(METER_ACTIVE_POWER, 0) // + .input(DEBUG_SET_ACTIVE_POWER, 0) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) // + .output(PUC_BATTERY_POWER, -18_000_000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, -118_000_000L) // + .output(LEVL_SOC, 18_000_000L)) // + .next(new TestCase() // + .input(ESS_SOC, 95) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, -18_000_000) // + .input(DEBUG_SET_ACTIVE_POWER, 0) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) // + .output(PUC_BATTERY_POWER, -18_000_000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, -136_000_000L) // + .output(LEVL_SOC, 0L)) // + .next(new TestCase() // + .input(ESS_SOC, 95) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, -18_000_000) // + .input(DEBUG_SET_ACTIVE_POWER, -18_000_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -18_000_000) // + .output(PUC_BATTERY_POWER, -18_000_000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, -136_000_000L) // + .output(LEVL_SOC, 0L)) // + .next(new TestCase() // + .input(ESS_SOC, 96) // + .input(ESS_ACTIVE_POWER, -18_000_000) // + .input(METER_ACTIVE_POWER, 0) // + .input(DEBUG_SET_ACTIVE_POWER, -18_000_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -18_000_000) // + .output(PUC_BATTERY_POWER, -18_000_000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, -136_000_000L) // + .output(LEVL_SOC, 0L)); // + } + + @Test + public void testLowerSocLimit() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withMaxApparentPower(20_000_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + // following values have to be initialized in the first cycle + .input(LEVL_SELL_TO_GRID_LIMIT, -40_000_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 40_000_000) // + .input(SOC_LOWER_BOUND_LEVL, 20) // + .input(SOC_UPPER_BOUND_LEVL, 80) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) // + .input(LEVL_EFFICIENCY, 100.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, 100_000_000) // + .input(LEVL_SOC, 0) // + // following values have to be updated each cycle + .input(ESS_SOC, 21) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 0) // + .input(DEBUG_SET_ACTIVE_POWER, 18_000_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 18_000_000) // + .output(PUC_BATTERY_POWER, -0L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 82_000_000L) // + .output(LEVL_SOC, -18_000_000L)) // + .next(new TestCase() // + .input(ESS_SOC, 20) // + .input(ESS_ACTIVE_POWER, 18_000_000) // + .input(METER_ACTIVE_POWER, -18_000_000) // + .input(DEBUG_SET_ACTIVE_POWER, 0) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) // + .output(PUC_BATTERY_POWER, 0L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 82_000_000L) // + .output(LEVL_SOC, -18_000_000L)) // + .next(new TestCase() // + .input(ESS_SOC, 20) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 0) // + .input(DEBUG_SET_ACTIVE_POWER, 0) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) // + .output(PUC_BATTERY_POWER, 0L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 82_000_000L) // + .output(LEVL_SOC, -18_000_000L)); // + } + + @Test + public void testLowerSocLimit_levlHasDischarged() throws Exception { + final var clock = createDummyClock(); + var now = Instant.now(clock); + + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1)) // + .withCapacity(500000) // 1.800.000.000 Ws + .withMaxApparentPower(30_000_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) // + .next(new TestCase() // + // following values have to be initialized in the first cycle + .input(LEVL_SELL_TO_GRID_LIMIT, -40_000_000) // + .input(LEVL_BUY_FROM_GRID_LIMIT, 40_000_000) // + .input(SOC_LOWER_BOUND_LEVL, 5) // + .input(SOC_UPPER_BOUND_LEVL, 95) // + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) // + .input(LEVL_EFFICIENCY, 100.0) // + .input(LEVL_REMAINING_LEVL_ENERGY, 100_000_000) // + .input(LEVL_SOC, -36_000_000) // 2% + // following values have to be updated each cycle + .input(ESS_SOC, 6) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 18_000_000) // + .input(DEBUG_SET_ACTIVE_POWER, 18_000_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 18_000_000) // + .output(PUC_BATTERY_POWER, 18_000_000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 100_000_000L) // + .output(LEVL_SOC, -36_000_000L)) // + .next(new TestCase() // + .input(ESS_SOC, 5) // + .input(ESS_ACTIVE_POWER, 18_000_000) // + .input(METER_ACTIVE_POWER, 0) // + .input(DEBUG_SET_ACTIVE_POWER, 0) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) // + .output(PUC_BATTERY_POWER, 18_000_000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 118_000_000L) // + .output(LEVL_SOC, -18_000_000L)) // + .next(new TestCase() // + .input(ESS_SOC, 5) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 18_000_000) // + .input(DEBUG_SET_ACTIVE_POWER, 0) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) // + .output(PUC_BATTERY_POWER, 18_000_000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 136_000_000L) // + .output(LEVL_SOC, 0L)) // + .next(new TestCase() // + .input(ESS_SOC, 5) // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 18_000_000) // + .input(DEBUG_SET_ACTIVE_POWER, 18_000_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 18_000_000) // + .output(PUC_BATTERY_POWER, 18_000_000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 136_000_000L) // + .output(LEVL_SOC, 0L)) // + .next(new TestCase() // + .input(ESS_SOC, 4) // + .input(ESS_ACTIVE_POWER, 18_000_000) // + .input(METER_ACTIVE_POWER, 0) // + .input(DEBUG_SET_ACTIVE_POWER, 18_000_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 18_000_000) // + .output(PUC_BATTERY_POWER, 18_000_000L) // + .output(LEVL_REMAINING_LEVL_ENERGY, 136_000_000L) // + .output(LEVL_SOC, 0L)); // + } +} diff --git a/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/ControllerEssBalancingImplTest.java b/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/ControllerEssBalancingImplTest.java new file mode 100644 index 00000000000..84f19333527 --- /dev/null +++ b/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/ControllerEssBalancingImplTest.java @@ -0,0 +1,317 @@ +package io.openems.edge.levl.controller; + +import static io.openems.edge.common.test.TestUtils.createDummyClock; + +import java.time.Clock; +import java.time.Instant; +import java.util.HashMap; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.osgi.service.event.Event; + +import com.google.gson.JsonObject; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.GenericJsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcResponse; +import io.openems.edge.common.channel.internal.AbstractReadChannel; +import io.openems.edge.common.event.EdgeEventConstants; +import io.openems.edge.common.jsonapi.Call; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyCycle; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.ess.test.DummyPower; +import io.openems.edge.meter.test.DummyElectricityMeter; + +public class ControllerEssBalancingImplTest { + + private ControllerEssBalancingImpl underTest; + + @Before + public void setUp() { + this.underTest = new ControllerEssBalancingImpl(); + } + + @Test + public void testCalculateRequiredPower() throws OpenemsNamedException { + this.underTest.cycle = new DummyCycle(1000); + this.underTest.ess = new DummyManagedSymmetricEss("ess0").setPower(new DummyPower(0.3, 0.3, 0.1)) + .withActivePower(-100).withCapacity(500) // 1.800.000 Ws + .withSoc(50) // 900.000 Ws + .withMaxApparentPower(500); + this.underTest.meter = new DummyElectricityMeter("meter0").withActivePower(200); + + this.setActiveChannelValue(this.underTest.getLevlSocChannel(), 2000L); + this.setActiveChannelValue(this.underTest.getRemainingLevlEnergyChannel(), 200000L); + this.setActiveChannelValue(this.underTest.getEssEfficiencyChannel(), 100.0); + this.setActiveChannelValue(this.underTest.getSocLowerBoundLevlChannel(), 20.0); + this.setActiveChannelValue(this.underTest.getSocUpperBoundLevlChannel(), 80.0); + this.setActiveChannelValue(this.underTest.getBuyFromGridLimitChannel(), 1000L); + this.setActiveChannelValue(this.underTest.getSellToGridLimitChannel(), -1000L); + this.setActiveChannelValue(this.underTest.getInfluenceSellToGridChannel(), true); + + int result = this.underTest.calculateRequiredPower(); + + Assert.assertEquals(500, result); + } + + // Primary use case calculation + @Test + public void testApplyPucSocBounds() { + Assert.assertEquals("good case discharge", 30, this.underTest.applyPucSocBounds(30, 50, 100, 100, 1)); + Assert.assertEquals("good case charge", -30, this.underTest.applyPucSocBounds(-30, 50, 100, 100, 1)); + Assert.assertEquals("minimum limit applies", 50, this.underTest.applyPucSocBounds(70, 50, 100, 100, 1)); + Assert.assertEquals("minimum limit applies due to cycleTime", 25, + this.underTest.applyPucSocBounds(30, 50, 100, 100, 2)); + Assert.assertEquals("maximum limit applies", -50, this.underTest.applyPucSocBounds(-70, 50, 100, 100, 1)); + Assert.assertEquals("no charging allowed because soc is 100%", 0, + this.underTest.applyPucSocBounds(-20, 100, 100, 100, 1)); + Assert.assertEquals("no discharging allowed because soc is 0%", 0, + this.underTest.applyPucSocBounds(20, 0, 100, 100, 1)); + Assert.assertEquals("discharging allowed with soc 100%", 20, + this.underTest.applyPucSocBounds(20, 100, 100, 100, 1)); + Assert.assertEquals("charging allowed with soc 0%", -20, this.underTest.applyPucSocBounds(-20, 0, 100, 100, 1)); + + // efficiency 80% + Assert.assertEquals("good case discharge /w efficiency", 30, + this.underTest.applyPucSocBounds(30, 50, 100, 80, 1)); + Assert.assertEquals("good case charge /w efficiency", -30, + this.underTest.applyPucSocBounds(-30, 50, 100, 80, 1)); + Assert.assertEquals("minimum limit applies /w efficiency", 40, + this.underTest.applyPucSocBounds(70, 50, 100, 80, 1)); + Assert.assertEquals("maximum limit applies /w efficiency", -62, + this.underTest.applyPucSocBounds(-70, 50, 100, 80, 1)); + } + + @Test + public void testCalculatePucBatteryPower() { + Assert.assertEquals("discharge within battery limit", 70, + this.underTest.calculatePucBatteryPower(50, 20, 500, 1000, -150, 150, 100, 1)); + Assert.assertEquals("discharge outside battery limit", 150, + this.underTest.calculatePucBatteryPower(200, 20, 500, 1000, -150, 150, 100, 1)); + Assert.assertEquals("charge outside battery limit", -150, + this.underTest.calculatePucBatteryPower(-200, -20, 500, 1000, -150, 150, 100, 1)); + } + + // Levl Power calculation + @Test + public void testApplyBatteryPowerLimitsToLevlPower() { + Assert.assertEquals(70, this.underTest.applyBatteryPowerLimitsToLevlPower(100, 30, -100, 100)); + Assert.assertEquals(50, this.underTest.applyBatteryPowerLimitsToLevlPower(50, 30, -100, 100)); + Assert.assertEquals(-100, this.underTest.applyBatteryPowerLimitsToLevlPower(-100, 30, -100, 100)); + Assert.assertEquals(-130, this.underTest.applyBatteryPowerLimitsToLevlPower(-150, 30, -100, 100)); + } + + @Test + public void testApplySocBoundariesToLevlPower() { + Assert.assertEquals(-22, this.underTest.applySocBoundariesToLevlPower(-100, 60, 0, 20, 80, 100, 90, 1)); + Assert.assertEquals(-10, this.underTest.applySocBoundariesToLevlPower(-10, 60, 0, 20, 80, 100, 90, 1)); + Assert.assertEquals(10, this.underTest.applySocBoundariesToLevlPower(10, 60, 0, 20, 80, 100, 90, 1)); + Assert.assertEquals(36, this.underTest.applySocBoundariesToLevlPower(100, 60, 0, 20, 80, 100, 90, 1)); + } + + @Test + public void testApplyGridPowerLimitsToLevlPower() { + Assert.assertEquals("levlPower within limits", 50, + this.underTest.applyGridPowerLimitsToLevlPower(50, 0, 80, -70)); + Assert.assertEquals("levlPower within limits balancing grid", 100, + this.underTest.applyGridPowerLimitsToLevlPower(100, 40, 80, -70)); + Assert.assertEquals("levlPower constraint by sellToGridLimit", 50, + this.underTest.applyGridPowerLimitsToLevlPower(100, -20, 80, -70)); + Assert.assertEquals("levlPower constraint by buyFromGridLimit", -60, + this.underTest.applyGridPowerLimitsToLevlPower(-100, 20, 80, -70)); + } + + @Test + public void testInfluenceSellToGridConstraint() { + Assert.assertEquals("influence allowed", 50, this.underTest.applyInfluenceSellToGridConstraint(50, 0, true)); + + Assert.assertEquals("buy from grid is allowed", -50, + this.underTest.applyInfluenceSellToGridConstraint(-50, 20, false)); + Assert.assertEquals("switch gridPower /w buy from grid to sell to grid not allowed", 20, + this.underTest.applyInfluenceSellToGridConstraint(50, 20, false)); + Assert.assertEquals("do nothing because grid power sells to grid", 0, + this.underTest.applyInfluenceSellToGridConstraint(-50, -20, false)); + } + + @Test + public void testHandleEvent_before_currentActive() { + Clock clock = createDummyClock(); + this.underTest.componentManager = new DummyComponentManager(clock); + + LevlControlRequest currentRequest = new LevlControlRequest(); + currentRequest.start = Instant.now(clock); //2020-01-01T00:00:00 + currentRequest.deadline = Instant.now(clock).plusSeconds(899); //2020-01-01T00:14:59 + this.underTest.currentRequest = currentRequest; + + LevlControlRequest nextRequest = new LevlControlRequest(); + nextRequest.start = Instant.now(clock).plusSeconds(900); //2020-01-01T00:15:00 + nextRequest.deadline = Instant.now(clock).plusSeconds(900 + 899); //2020-01-01T00:29:59 + this.underTest.nextRequest = nextRequest; + + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); + + this.underTest.handleEvent(event); + + Assert.assertEquals(currentRequest, this.underTest.currentRequest); + } + + @Test + public void testHandleEvent_before_nextRequestIsActive() { + Clock clock = createDummyClock(); + this.underTest.componentManager = new DummyComponentManager(clock); + + LevlControlRequest currentRequest = new LevlControlRequest(); + currentRequest.start = Instant.now(clock).minusSeconds(900); //2019-12-31T23:45:00 + currentRequest.deadline = Instant.now(clock).minusSeconds(1); //2019-12-31T23:59:59 + this.underTest.currentRequest = currentRequest; + + LevlControlRequest nextRequest = new LevlControlRequest(); + nextRequest.start = Instant.now(clock); //2020-01-01T00:00:00 + nextRequest.deadline = Instant.now(clock).plusSeconds(899); //2020-01-01T00:14:59 + this.underTest.nextRequest = nextRequest; + + this.setNextChannelValue(this.underTest.getRealizedEnergyGridChannel(), 100L); + this.setNextChannelValue(this.underTest.getRealizedEnergyBatteryChannel(), 200L); + + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); + + this.underTest.handleEvent(event); + + Assert.assertEquals(nextRequest, this.underTest.currentRequest); + Assert.assertNull(this.underTest.nextRequest); + Assert.assertEquals(0, this.underTest.getRealizedEnergyGridChannel().getNextValue().get().longValue()); + Assert.assertEquals(0, this.underTest.getRealizedEnergyBatteryChannel().getNextValue().get().longValue()); + } + + @Test + public void testHandleEvent_before_gapBetweenRequests() { + Clock clock = createDummyClock(); + this.underTest.componentManager = new DummyComponentManager(clock); + + LevlControlRequest currentRequest = new LevlControlRequest(); + currentRequest.start = Instant.now(clock).minusSeconds(900); //2019-12-31T23:45:00 + currentRequest.deadline = Instant.now(clock).minusSeconds(1); //2019-12-31T23:59:59 + this.underTest.currentRequest = currentRequest; + + LevlControlRequest nextRequest = new LevlControlRequest(); + nextRequest.start = Instant.now(clock).plusSeconds(60); //2020-01-01T00:01:00 + nextRequest.deadline = Instant.now(clock).plusSeconds(899);; //2020-01-01T00:14:59 + this.underTest.nextRequest = nextRequest; + + this.setNextChannelValue(this.underTest.getRealizedEnergyGridChannel(), 100L); + this.setNextChannelValue(this.underTest.getRealizedEnergyBatteryChannel(), 200L); + + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); + + this.underTest.handleEvent(event); + + Assert.assertNull(this.underTest.currentRequest); + Assert.assertEquals(nextRequest, this.underTest.nextRequest); + Assert.assertEquals(0, this.underTest.getRealizedEnergyGridChannel().getNextValue().get().longValue()); + Assert.assertEquals(0, this.underTest.getRealizedEnergyBatteryChannel().getNextValue().get().longValue()); + } + + @Test + public void testHandleEvent_before_noNextRequest() { + Clock clock = createDummyClock(); + this.underTest.componentManager = new DummyComponentManager(clock); + + LevlControlRequest currentRequest = new LevlControlRequest(); + currentRequest.start = Instant.now(clock).minusSeconds(900); //2019-12-31T23:45:00 + currentRequest.deadline = Instant.now(clock).minusSeconds(1); //2019-12-31T23:59:59 + this.underTest.currentRequest = currentRequest; + + this.setNextChannelValue(this.underTest.getRealizedEnergyGridChannel(), 100L); + this.setNextChannelValue(this.underTest.getRealizedEnergyBatteryChannel(), 200L); + + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); + + this.underTest.handleEvent(event); + + Assert.assertNull(this.underTest.currentRequest); + Assert.assertNull(this.underTest.nextRequest); + Assert.assertEquals(0, this.underTest.getRealizedEnergyGridChannel().getNextValue().get().longValue()); + Assert.assertEquals(0, this.underTest.getRealizedEnergyBatteryChannel().getNextValue().get().longValue()); + } + + @Test + public void testHandleEvent_before_noRequests() { + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); + + Clock clock = createDummyClock(); + this.underTest.componentManager = new DummyComponentManager(clock); + + this.underTest.handleEvent(event); + + Assert.assertNull(this.underTest.currentRequest); + Assert.assertNull(this.underTest.nextRequest); + } + + @Test + public void testHandleEvent_after() { + this.underTest.ess = new DummyManagedSymmetricEss("ess0"); + this.underTest.cycle = new DummyCycle(1000); + this.setNextChannelValue(this.underTest.ess.getDebugSetActivePowerChannel(), -100); + this.setNextChannelValue(this.underTest.getPucBatteryPowerChannel(), 10L); + this.setActiveChannelValue(this.underTest.getLevlSocChannel(), 40L); + this.setActiveChannelValue(this.underTest.getRemainingLevlEnergyChannel(), -1000L); + this.setActiveChannelValue(this.underTest.getEssEfficiencyChannel(), 80.0); + this.underTest.currentRequest = new LevlControlRequest(); + + this.setActiveChannelValue(this.underTest.getRealizedEnergyGridChannel(), -20L); + this.setActiveChannelValue(this.underTest.getRealizedEnergyBatteryChannel(), -30L); + + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, new HashMap<>()); + + this.underTest.handleEvent(event); + + Assert.assertEquals(-890, this.underTest.getRemainingLevlEnergyChannel().getNextValue().get().longValue()); + Assert.assertEquals(-130, this.underTest.getRealizedEnergyGridChannel().getNextValue().get().longValue()); + Assert.assertEquals(-118, this.underTest.getRealizedEnergyBatteryChannel().getNextValue().get().longValue()); + Assert.assertEquals(128, this.underTest.getLevlSocChannel().getNextValue().get().longValue()); + } + + @Test + public void testHandleRequest() throws OpenemsNamedException { + JsonObject params = new JsonObject(); + params.addProperty("levlRequestId", "id"); + params.addProperty("levlRequestTimestamp", "2020-01-01T00:15:00Z"); + params.addProperty("levlPowerW", 500); + params.addProperty("levlChargeDelaySec", 900); + params.addProperty("levlChargeDurationSec", 899); + params.addProperty("levlSocWh", 10000); + params.addProperty("levlSocLowerBoundPercent", 20); + params.addProperty("levlSocUpperBoundPercent", 80); + params.addProperty("sellToGridLimitW", 3000); + params.addProperty("buyFromGridLimitW", 4000); + params.addProperty("efficiencyPercent", 90); + params.addProperty("influenceSellToGrid", true); + JsonrpcRequest request = new GenericJsonrpcRequest("sendLevlControlRequest", params); + Call