From 2ce884e38ed9f15a794e4d9046b27a60b610e323 Mon Sep 17 00:00:00 2001 From: Luca Mancilik Date: Mon, 21 Oct 2024 17:46:35 +0200 Subject: [PATCH 01/41] initial commit --- io.openems.edge.levl.controller/.classpath | 12 ++ io.openems.edge.levl.controller/.gitignore | 3 + io.openems.edge.levl.controller/.project | 23 ++++ .../org.eclipse.core.resources.prefs | 2 + io.openems.edge.levl.controller/bnd.bnd | 15 +++ io.openems.edge.levl.controller/readme.adoc | 16 +++ .../openems/edge/levl/controller/Config.java | 44 ++++++ .../controller/ControllerEssBalancing.java | 23 ++++ .../ControllerEssBalancingImpl.java | 126 ++++++++++++++++++ .../levl/controller/LevlControlRequest.java | 102 ++++++++++++++ .../edge/levl/controller/RequestHandler.java | 43 ++++++ .../edge/levl/controller/common/Limit.java | 84 ++++++++++++ .../test/.gitignore | 0 .../ess/balancing/BalancingImplTest.java | 97 ++++++++++++++ .../controller/ess/balancing/MyConfig.java | 84 ++++++++++++ 15 files changed, 674 insertions(+) create mode 100644 io.openems.edge.levl.controller/.classpath create mode 100644 io.openems.edge.levl.controller/.gitignore create mode 100644 io.openems.edge.levl.controller/.project create mode 100644 io.openems.edge.levl.controller/.settings/org.eclipse.core.resources.prefs create mode 100644 io.openems.edge.levl.controller/bnd.bnd create mode 100644 io.openems.edge.levl.controller/readme.adoc create mode 100644 io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java create mode 100644 io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java create mode 100644 io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java create mode 100644 io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java create mode 100644 io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java create mode 100644 io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Limit.java create mode 100644 io.openems.edge.levl.controller/test/.gitignore create mode 100644 io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/BalancingImplTest.java create mode 100644 io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/MyConfig.java diff --git a/io.openems.edge.levl.controller/.classpath b/io.openems.edge.levl.controller/.classpath new file mode 100644 index 00000000000..bbfbdbe40e7 --- /dev/null +++ b/io.openems.edge.levl.controller/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/io.openems.edge.levl.controller/.gitignore b/io.openems.edge.levl.controller/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/io.openems.edge.levl.controller/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/io.openems.edge.levl.controller/.project b/io.openems.edge.levl.controller/.project new file mode 100644 index 00000000000..97f88498450 --- /dev/null +++ b/io.openems.edge.levl.controller/.project @@ -0,0 +1,23 @@ + + + io.openems.edge.levl.controller + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.edge.levl.controller/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.levl.controller/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/io.openems.edge.levl.controller/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/io.openems.edge.levl.controller/bnd.bnd b/io.openems.edge.levl.controller/bnd.bnd new file mode 100644 index 00000000000..1550b950bae --- /dev/null +++ b/io.openems.edge.levl.controller/bnd.bnd @@ -0,0 +1,15 @@ +Bundle-Name: OpenEMS Edge Controller Ess Balancing +Bundle-Vendor: FENECON GmbH +Bundle-License: https://opensource.org/licenses/EPL-2.0 +Bundle-Version: 1.0.0.${tstamp} + +-buildpath: \ + ${buildpath},\ + io.openems.common,\ + io.openems.edge.common,\ + io.openems.edge.controller.api,\ + io.openems.edge.ess.api,\ + io.openems.edge.meter.api + +-testpath: \ + ${testpath} diff --git a/io.openems.edge.levl.controller/readme.adoc b/io.openems.edge.levl.controller/readme.adoc new file mode 100644 index 00000000000..11bf25f0227 --- /dev/null +++ b/io.openems.edge.levl.controller/readme.adoc @@ -0,0 +1,16 @@ += ESS Balancing + +This controls a energy storage system in self-consumption optimization mode by trying to balance the power at the grid connection point, i.e.: +- charge the battery, when production is larger than consumption (i.e. when feeding power to the grid) +- discharge the battery, when consumption is larger than production (i.e. when buying power from the grid) + +== Requirements + +** ManagedSymmetricEss, a controllable energy storage system +** ElectricityMeter, a meter at the grid connection point + +== Additional application notes + +Above description assumes that the grid connection point should be balanced to `0 Watt`. This bevahiour is configurable using the `targetGridSetpoint` configuration parameter. + +https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.controller.ess.balancing[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java new file mode 100644 index 00000000000..0370fff5093 --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java @@ -0,0 +1,44 @@ +package io.openems.edge.levl.controller; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(// + name = "Levl Controller Ess Balancing", // + description = "Optimizes the self-consumption by keeping the grid meter on zero.") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "ctrlLevlBalancing0"; + + @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") + String alias() default ""; + + @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") + boolean enabled() default true; + + @AttributeDefinition(name = "Ess-ID", description = "ID of Ess device.") + String ess_id(); + + @AttributeDefinition(name = "Grid-Meter-ID", description = "ID of the Grid-Meter.") + String meter_id(); + + @AttributeDefinition(name = "Target Grid Setpoint", description = "The target setpoint for grid. Positive for buy-from-grid; negative for sell-to-grid.") + int targetGridSetpoint() default 0; + + @AttributeDefinition(name = "Ess target filter", description = "This is auto-generated by 'Ess-ID'.") + String ess_target() default "(enabled=true)"; + + @AttributeDefinition(name = "Meter target filter", description = "This is auto-generated by 'Meter-ID'.") + String meter_target() default "(enabled=true)"; + + String webconsole_configurationFactory_nameHint() default "Controller Ess Balancing [{id}]"; + + @AttributeDefinition(name = "Physical-SoC-Lower-Bound-Percent", description = "Physical lower SoC Bound (%) of the battery") + int physical_soc_lower_bound_percent() default 0; + + @AttributeDefinition(name = "Physical-SoC-Upper-Bound-Percent", description = "Physical upper SoC Bound (%) of the battery") + int physical_soc_upper_bound_percent() default 100; + + +} \ No newline at end of file diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java new file mode 100644 index 00000000000..a1d99bddcd7 --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -0,0 +1,23 @@ +package io.openems.edge.levl.controller; + +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.controller.api.Controller; + +public interface ControllerEssBalancing extends Controller, OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + ; + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + +} \ No newline at end of file diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java new file mode 100644 index 00000000000..509ecd55ea6 --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -0,0 +1,126 @@ +package io.openems.edge.levl.controller; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.jsonapi.ComponentJsonApi; +import io.openems.edge.common.jsonapi.JsonApiBuilder; +import io.openems.edge.controller.api.Controller; +import io.openems.edge.ess.api.ManagedSymmetricEss; +import io.openems.edge.meter.api.ElectricityMeter; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Levl.Controller.Symmetric.Balancing", // This name has to be kept for compatibility reasons + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class ControllerEssBalancingImpl extends AbstractOpenemsComponent implements Controller, OpenemsComponent, ComponentJsonApi { + + private final Logger log = LoggerFactory.getLogger(ControllerEssBalancingImpl.class); + + @Reference + private ConfigurationAdmin cm; + + @Reference + private ManagedSymmetricEss ess; + + @Reference + private ElectricityMeter meter; + + private Config config; + + private RequestHandler requestHandler; + + private static final String METHOD = "sendLevlControlRequest"; + + public ControllerEssBalancingImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values(), // + ControllerEssBalancing.ChannelId.values() // + ); + } + + @Activate + private void activate(ComponentContext context, Config config) { + super.activate(context, config.id(), config.alias(), config.enabled()); + this.config = config; + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "ess", config.ess_id())) { + return; + } + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "meter", config.meter_id())) { + return; + } + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + public void run() throws OpenemsNamedException { + /* + * Check that we are On-Grid (and warn on undefined Grid-Mode) + */ + var gridMode = this.ess.getGridMode(); + if (gridMode.isUndefined()) { + this.logWarn(this.log, "Grid-Mode is [UNDEFINED]"); + } + switch (gridMode) { + case ON_GRID: + case UNDEFINED: + break; + case OFF_GRID: + return; + } + + /* + * Calculates required charge/discharge power + */ + var calculatedPower = calculateRequiredPower(// + this.ess.getActivePower().getOrError(), // + this.meter.getActivePower().getOrError(), // + this.config.targetGridSetpoint()); + + /* + * set result + */ + this.ess.setActivePowerEqualsWithPid(calculatedPower); + this.ess.setReactivePowerEquals(0); + } + + /** + * Calculates required charge/discharge power. + * + * @param essPower the charge/discharge power of the + * {@link ManagedSymmetricEss} + * @param gridPower the buy-from-grid/sell-to grid power + * @param targetGridSetpoint the configured targetGridSetpoint + * @return the required power + */ + protected static int calculateRequiredPower(int essPower, int gridPower, int targetGridSetpoint) { + return gridPower + essPower - targetGridSetpoint; + } + + @Override + public void buildJsonApiRoutes(JsonApiBuilder builder) { + builder.handleRequest(METHOD, call -> { + return this.requestHandler.addRequest(call.getRequest()); + }); + } + +} diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java new file mode 100644 index 00000000000..2cb007fa762 --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -0,0 +1,102 @@ +package io.openems.edge.levl.controller; + +import com.google.gson.JsonObject; +import io.openems.common.exceptions.OpenemsError; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.edge.levl.controller.common.Limit; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +public class LevlControlRequest extends JsonrpcRequest { + public static final String METHOD = "sendLevlControlRequest"; + private int sellToGridLimitW; + private int buyFromGridLimitW; + private String levlRequestId; + private String levlRequestTimestamp; + private int levlPowerW; + private int levlChargeDelaySec; + private int levlChargeDurationSec; + private int totalRealizedDischargeEnergyWh; + private int levlSocLowerBoundPercent; + private int levlSocUpperBoundPercent; + private BigDecimal efficiencyPercent; + private boolean influenceSellToGrid; + private final JsonObject params; + + /** + * Creates a new LevlControlRequest from a JsonrpcRequest. + * + * @param request the JsonrpcRequest + * @return a new LevlControlRequest + * @throws OpenemsError.OpenemsNamedException if the request is invalid + */ + public static LevlControlRequest from(JsonrpcRequest request) throws OpenemsError.OpenemsNamedException { + var params = request.getParams(); + return new LevlControlRequest(params); + } + + /** + * Creates a new LevlControlRequest from a JsonObject. + * + * @param params the JsonObject + * @throws OpenemsError.OpenemsNamedException if the request is invalid + */ + public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedException { + super(LevlControlRequest.METHOD); + this.params = params; + try { + this.parseFields(params); + } catch (NullPointerException e) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("missing fields in request: " + e.getMessage()); + } catch (NumberFormatException e) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("wrong field type in request: " + e.getMessage()); + } + if (this.efficiencyPercent.compareTo(BigDecimal.ZERO) <= 0) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be > 0"); + } + if (this.efficiencyPercent.compareTo(BigDecimal.valueOf(100)) > 0) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be <= 100"); + } + } + + /** + * Parses the fields from a JsonObject. + * + * @param params the JsonObject + */ + private void parseFields(JsonObject params) { + this.levlRequestId = params.get("levlRequestId").getAsString(); + this.levlRequestTimestamp = params.get("levlRequestTimestamp").getAsString(); + this.levlPowerW = params.get("levlPowerW").getAsInt(); + this.levlChargeDelaySec = params.get("levlChargeDelaySec").getAsInt(); + this.levlChargeDurationSec = params.get("levlChargeDurationSec").getAsInt(); + this.totalRealizedDischargeEnergyWh = -params.get("levlSocWh").getAsInt(); + this.levlSocLowerBoundPercent = params.get("levlSocLowerBoundPercent").getAsInt(); + this.levlSocUpperBoundPercent = params.get("levlSocUpperBoundPercent").getAsInt(); + this.sellToGridLimitW = params.get("sellToGridLimitW").getAsInt(); + this.buyFromGridLimitW = params.get("buyFromGridLimitW").getAsInt(); + this.efficiencyPercent = params.get("efficiencyPercent").getAsBigDecimal(); + this.influenceSellToGrid = params.has("influenceSellToGrid") + ? params.get("influenceSellToGrid").getAsBoolean() + : true; + } + + /** + * Creates a new Limit instance. + * + * @return a new Limit instance + */ + public Limit createGridPowerLimitW() { + return new Limit(this.sellToGridLimitW, this.buyFromGridLimitW); + } + + public String getLevlRequestId() { + return this.levlRequestId; + } + + @Override + public JsonObject getParams() { + return this.params; + } +} \ No newline at end of file diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java new file mode 100644 index 00000000000..0a6fc9a443e --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java @@ -0,0 +1,43 @@ +package io.openems.edge.levl.controller; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import com.google.gson.JsonObject; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcResponse; +import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; + +public class RequestHandler { + + private List requests; + + public RequestHandler() { + this.requests = new ArrayList<>(); + } + + public JsonrpcResponse addRequest(JsonrpcRequest request) throws OpenemsNamedException { + var levlControlRequest = LevlControlRequest.from(request); + this.requests.add(request); + return JsonrpcResponseSuccess + .from(this.generateResponse(request.getId(), levlControlRequest.getLevlRequestId())); + } + + private JsonObject generateResponse(UUID requestId, String levlRequestId) { + JsonObject response = new JsonObject(); + var result = new JsonObject(); + result.addProperty("levlRequestId", levlRequestId); + response.addProperty("id", requestId.toString()); + response.add("result", result); + return response; + } + + // TODO implement me + protected JsonrpcRequest getActiveRequest() { + return this.requests.get(0); + } + +} diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Limit.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Limit.java new file mode 100644 index 00000000000..576ccec1d16 --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Limit.java @@ -0,0 +1,84 @@ +package io.openems.edge.levl.controller.common; + +/** + * A class representing a limit with a minimum and maximum power. + */ +public record Limit(int minPower, int maxPower) { + + /** + * Returns a new Limit with the minimum and maximum power set to the minimum and maximum values of an integer. + * + * @return a new Limit + */ + public static Limit unconstrained() { + return new Limit(Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + /** + * Returns a new Limit with the minimum power set to the given bound and the maximum power set to the maximum value of an integer. + * + * @param bound the minimum power + * @return a new Limit + */ + public static Limit lowerBound(int bound) { + return new Limit(bound, Integer.MAX_VALUE); + } + + /** + * Returns a new Limit with the minimum power set to the minimum value of an integer and the maximum power set to the given bound. + * + * @param bound the maximum power + * @return a new Limit + */ + public static Limit upperBound(int bound) { + return new Limit(Integer.MIN_VALUE, bound); + } + + /** + * Returns the given value constrained by the minimum and maximum power of this Limit. + * + * @param value the value to constrain + * @return the constrained value + */ + public int apply(int value) { + return Math.max(Math.min(value, this.maxPower), this.minPower); + } + + /** + * Returns a new Limit with the maximum of the minimum powers and the minimum of the maximum powers of this Limit and the given Limit. + * + * @param otherLimit the other Limit + * @return a new Limit + */ + public Limit intersect(Limit otherLimit) { + return new Limit(Math.max(this.minPower, otherLimit.minPower), Math.min(this.maxPower, otherLimit.maxPower)); + } + + /** + * Returns a new Limit with the negative of the maximum power as the minimum power and the negative of the minimum power as the maximum power. + * + * @return a new Limit + */ + public Limit invert() { + return new Limit(-this.maxPower, -this.minPower); + } + + /** + * Returns a new Limit with the minimum and maximum power of this Limit shifted by the given delta. + * + * @param delta the amount to shift by + * @return a new Limit + */ + public Limit shiftBy(int delta) { + return new Limit(this.minPower + delta, this.maxPower + delta); + } + + /** + * Returns a new Limit with the minimum power as the minimum of 0 and the minimum power of this Limit and the maximum power as the maximum of 0 and the maximum power of this Limit. + * + * @return a new Limit + */ + public Limit ensureValidLimitWithZero() { + return new Limit(Math.min(0, this.minPower), Math.max(0, this.maxPower)); + } +} \ 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/controller/ess/balancing/BalancingImplTest.java b/io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/BalancingImplTest.java new file mode 100644 index 00000000000..56301add84b --- /dev/null +++ b/io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/BalancingImplTest.java @@ -0,0 +1,97 @@ +package io.openems.edge.controller.ess.balancing; + +import org.junit.Test; + +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.DummyConfigurationAdmin; +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.levl.controller.ControllerEssBalancingImpl; +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_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, + "SetActivePowerEquals"); + + private static final String METER_ID = "meter0"; + private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); + + @Test + public void test() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // + .setPower(new DummyPower(0.3, 0.3, 0.1))) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .setTargetGridSetpoint(0) // + .build()) + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, 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)); + } + +} diff --git a/io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/MyConfig.java b/io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/MyConfig.java new file mode 100644 index 00000000000..cea80fb35fc --- /dev/null +++ b/io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/MyConfig.java @@ -0,0 +1,84 @@ +package io.openems.edge.controller.ess.balancing; + +import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.utils.ConfigUtils; +import io.openems.edge.levl.controller.Config; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private String essId; + private String meterId; + private int targetGridSetpoint; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setEssId(String essId) { + this.essId = essId; + return this; + } + + public Builder setMeterId(String meterId) { + this.meterId = meterId; + return this; + } + + public Builder setTargetGridSetpoint(int targetGridSetpoint) { + this.targetGridSetpoint = targetGridSetpoint; + return this; + } + + public MyConfig build() { + return new MyConfig(this); + } + } + + /** + * Create a Config builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + private final Builder builder; + + private MyConfig(Builder builder) { + super(Config.class, builder.id); + this.builder = builder; + } + + @Override + public String ess_id() { + return this.builder.essId; + } + + @Override + public String meter_id() { + return this.builder.meterId; + } + + @Override + public int targetGridSetpoint() { + return this.builder.targetGridSetpoint; + } + + @Override + public String ess_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.ess_id()); + } + + @Override + public String meter_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.meter_id()); + } +} \ No newline at end of file From 308857bad4528415721ec0f78bc0847135fb9d7b Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Tue, 22 Oct 2024 17:33:42 +0200 Subject: [PATCH 02/41] levl-1290/1295: add business logic and json rpc handling --- io.openems.edge.levl.controller/bnd.bnd | 3 +- .../controller/ControllerEssBalancing.java | 63 ++++- .../ControllerEssBalancingImpl.java | 224 +++++++++++++++++- .../levl/controller/LevlControlRequest.java | 46 ++-- .../edge/levl/controller/RequestHandler.java | 13 +- 5 files changed, 313 insertions(+), 36 deletions(-) diff --git a/io.openems.edge.levl.controller/bnd.bnd b/io.openems.edge.levl.controller/bnd.bnd index 1550b950bae..22e747a769f 100644 --- a/io.openems.edge.levl.controller/bnd.bnd +++ b/io.openems.edge.levl.controller/bnd.bnd @@ -9,7 +9,8 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.common,\ io.openems.edge.controller.api,\ io.openems.edge.ess.api,\ - io.openems.edge.meter.api + io.openems.edge.meter.api,\ + io.openems.edge.ess.generic -testpath: \ ${testpath} diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java index a1d99bddcd7..057804e40ca 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -1,13 +1,26 @@ package io.openems.edge.levl.controller; +import io.openems.common.channel.PersistencePriority; +import io.openems.common.channel.Unit; +import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.IntegerReadChannel; +import io.openems.edge.common.channel.StringReadChannel; +import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.controller.api.Controller; public interface ControllerEssBalancing extends Controller, OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - ; + REALIZED_DISCHARGE_POWER_W(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.HIGH) + .text("the cumulated amount of discharge power that was realized since the last discharge request (in W)")), // + REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING) // + .persistencePriority(PersistencePriority.HIGH) + .text("the timestamp of the last levl control request")); + private final Doc doc; private ChannelId(Doc doc) { @@ -19,5 +32,53 @@ public Doc doc() { return this.doc; } } + + /** + * Returns the IntegerReadChannel for the realized power. + * @return the IntegerReadChannel + */ + public default IntegerReadChannel getRealizedPowerWChannel() { + return this.channel(ChannelId.REALIZED_DISCHARGE_POWER_W); + } + + /** + * Returns the value of the realized power. + * @return the value of the realized power + */ + public default Value getRealizedPowerW() { + return this.getRealizedPowerWChannel().value(); + } + + /** + * Sets the next value of the realized power. + * @param value the next value + */ + public default void _setRealizedPowerW(Integer value) { + this.getRealizedPowerWChannel().setNextValue(value); + } + + /** + * Returns the StringReadChannel for the last control request timestamp. + * @return the StringReadChannel + */ + public default StringReadChannel getLastControlRequestTimestampChannel() { + return this.channel(ChannelId.REQUEST_TIMESTAMP); + } + + /** + * Returns the value of the last control request timestamp. + * @return the value of the last control request timestamp + */ + public default Value getLastControlRequestTimestamp() { + return this.getLastControlRequestTimestampChannel().value(); + } + + /** + * Sets the next value of the last control request timestamp. + * @param value the next value + */ + public default void _setLastControlRequestTimestamp(String value) { + this.getLastControlRequestTimestampChannel().setNextValue(value); + } } \ No newline at end of file diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 509ecd55ea6..853f32bf50d 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -1,5 +1,9 @@ package io.openems.edge.levl.controller; +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.UUID; + import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -7,26 +11,47 @@ import org.osgi.service.component.annotations.ConfigurationPolicy; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventHandler; +import org.osgi.service.event.propertytypes.EventTopics; import org.osgi.service.metatype.annotations.Designate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.JsonObject; + +import io.openems.common.exceptions.InvalidValueException; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.cycle.Cycle; +import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.jsonapi.ComponentJsonApi; import io.openems.edge.common.jsonapi.JsonApiBuilder; import io.openems.edge.controller.api.Controller; import io.openems.edge.ess.api.ManagedSymmetricEss; +import io.openems.edge.ess.power.api.Phase; +import io.openems.edge.ess.power.api.Pwr; import io.openems.edge.meter.api.ElectricityMeter; +import io.openems.edge.levl.controller.common.Efficiency; + @Designate(ocd = Config.class, factory = true) -@Component(// - name = "Levl.Controller.Symmetric.Balancing", // This name has to be kept for compatibility reasons - immediate = true, // - configurationPolicy = ConfigurationPolicy.REQUIRE // +@Component( + name = "Levl.Controller.Symmetric.Balancing", + immediate = true, + configurationPolicy = ConfigurationPolicy.REQUIRE ) -public class ControllerEssBalancingImpl extends AbstractOpenemsComponent implements Controller, OpenemsComponent, ComponentJsonApi { +@EventTopics({ + EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, + EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, +}) + +//TODO: Channel erstellen um realizedValue des letzten Zyklus an Levl zu geben + +public class ControllerEssBalancingImpl extends AbstractOpenemsComponent implements Controller, OpenemsComponent, ComponentJsonApi, EventHandler { private final Logger log = LoggerFactory.getLogger(ControllerEssBalancingImpl.class); @@ -39,9 +64,24 @@ public class ControllerEssBalancingImpl extends AbstractOpenemsComponent impleme @Reference private ElectricityMeter meter; + @Reference + private Cycle cycle; + + private Config config; + private static Clock clock = Clock.systemDefaultZone(); + + private LevlControlRequest activeRequest; + private LevlControlRequest nextRequest; + private RequestHandler requestHandler; + private long levlSocWs; + private long realizedEnergyBatteryWs; + private long realizedEnergyGridWs; + private long realizedEnergyGridLastRequestWs; + + private int pucBatteryPower; private static final String METHOD = "sendLevlControlRequest"; @@ -91,10 +131,7 @@ public void run() throws OpenemsNamedException { /* * Calculates required charge/discharge power */ - var calculatedPower = calculateRequiredPower(// - this.ess.getActivePower().getOrError(), // - this.meter.getActivePower().getOrError(), // - this.config.targetGridSetpoint()); + var calculatedPower = calculateRequiredPower(this.config.targetGridSetpoint()); /* * set result @@ -111,16 +148,179 @@ public void run() throws OpenemsNamedException { * @param gridPower the buy-from-grid/sell-to grid power * @param targetGridSetpoint the configured targetGridSetpoint * @return the required power + * @throws InvalidValueException + */ + protected int calculateRequiredPower(int targetGridSetpoint) throws OpenemsNamedException { + float cycleTimeSec = this.cycle.getCycleTime() / 1000; + + // load physical values + int physicalSoc = this.ess.getSoc().getOrError(); + int gridPower = this.meter.getActivePower().getOrError(); + int essPower = this.ess.getActivePower().getOrError(); + int essCapacity = this.ess.getCapacity().getOrError(); + int minEssPower = this.ess.getPower().getMinPower(this.ess, Phase.ALL, Pwr.ACTIVE); + int maxEssPower = this.ess.getPower().getMaxPower(this.ess, Phase.ALL, Pwr.ACTIVE); + + long essCapacityWs = essCapacity * 3600; + long physicalSocWs = physicalSoc / 100 * essCapacityWs; + + + // ##### primary use case (puc) calculation + long pucSocWs = physicalSocWs - this.levlSocWs; + this.pucBatteryPower = calculatePucBatteryPower(targetGridSetpoint, cycleTimeSec, gridPower, essPower, essCapacityWs, pucSocWs, minEssPower, maxEssPower); + int pucGridPower = gridPower - essPower + this.pucBatteryPower; + long nextPucSocWs = pucSocWs + Math.round(this.pucBatteryPower * cycleTimeSec); + + //##### Levl calculation + int levlPowerW = this.calculateLevlPowerW(this.pucBatteryPower, minEssPower, maxEssPower, pucGridPower, nextPucSocWs, essCapacityWs, cycleTimeSec); + + //##### Overall calculation + long batteryPowerW = this.pucBatteryPower + levlPowerW; + return (int) batteryPowerW; + } + + /** + * Calculates the power of the primary use case (puc) + * + * @param targetGridSetpoint + * @param cycleTimeSec + * @param gridPower + * @param essPower + * @param essCapacityWs + * @param pucSocWs + * @param minEssPower + * @param maxEssPower + * @return + */ + private int calculatePucBatteryPower(int targetGridSetpoint, float cycleTimeSec, int gridPower, int essPower, + long essCapacityWs, long pucSocWs, int minEssPower, int maxEssPower) { + // calculate pucPower without any limits + int pucBatteryPower = gridPower + essPower - targetGridSetpoint; + + // apply ess power limits + pucBatteryPower = Math.max(Math.min(pucBatteryPower, maxEssPower), minEssPower); + + // apply soc bounds + pucBatteryPower = applyPucSocBounds(cycleTimeSec, essCapacityWs, pucSocWs, pucBatteryPower); + return pucBatteryPower; + } + + /** + * Checks and corrects the pucPower if it would exceed the upper or lower limits of the SoC. + * + * @param cycleTimeSec + * @param essCapacityWs + * @param pucSocWs + * @param pucPower + * @return the restricted pucPower */ - protected static int calculateRequiredPower(int essPower, int gridPower, int targetGridSetpoint) { - return gridPower + essPower - targetGridSetpoint; + private int applyPucSocBounds(float cycleTimeS, long essCapacityWs, long pucSocWs, int pucPower) { + //TODO: fix casting + //TODO: efficiency in if anwenden + if (pucSocWs - (pucPower * cycleTimeS) > essCapacityWs) { + pucPower = (int) -((essCapacityWs - pucSocWs) / cycleTimeS); + pucPower = (int) Efficiency.apply(Math.round(pucPower / cycleTimeS), this.activeRequest.efficiencyPercent); + } + if (pucSocWs - (pucPower * cycleTimeS) < 0) { + pucPower = (int) -((0 - pucSocWs) / cycleTimeS); + pucPower = (int) Efficiency.apply(Math.round(pucPower / cycleTimeS), this.activeRequest.efficiencyPercent); + } + return pucPower; + } + + private int calculateLevlPowerW(int pucBatteryPower, int minEssPower, int maxEssPower, int pucGridPower, long nextPucSocWs, long essCapacityWs, float cycleTimeS) { + long levlPower = Math.round((this.activeRequest.energyWs - this.realizedEnergyGridWs) / (double) cycleTimeS); + + levlPower = this.applyBatteryPowerLimitsToLevlPower(levlPower, pucBatteryPower, minEssPower, maxEssPower); + levlPower = this.applySocBoundariesToLevlPower(levlPower, nextPucSocWs, essCapacityWs, cycleTimeS); + levlPower = this.applyGridPowerLimitsToLevlPower(levlPower, pucGridPower); + + return (int) levlPower; + } + + private long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, int maxEssPower) { + int levlPowerLowerBound = minEssPower - pucBatteryPower; + int levlPowerUpperBound = maxEssPower - pucBatteryPower; + return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); + } + + private long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long essCapacityWs, float cycleTimeS) { + long levlSocLowerBoundWs = this.activeRequest.socLowerBoundPercent / 100 * essCapacityWs - nextPucSocWs; + long levlSocUpperBoundWs = this.activeRequest.socUpperBoundPercent / 100 * essCapacityWs - nextPucSocWs; + if (levlSocLowerBoundWs > 0) levlSocLowerBoundWs = 0; + if (levlSocUpperBoundWs < 0) levlSocUpperBoundWs = 0; + + long levlDischargeEnergyLowerBoundWs = -(levlSocUpperBoundWs - this.levlSocWs); + long levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - this.levlSocWs); + + long levlPowerLowerBound = Efficiency.apply(Math.round(levlDischargeEnergyLowerBoundWs / cycleTimeS), this.activeRequest.efficiencyPercent); + long levlPowerUpperBound = Efficiency.apply(Math.round(levlDischargeEnergyUpperBoundWs / cycleTimeS), this.activeRequest.efficiencyPercent); + + return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); + } + + private long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower) { + long levlPowerLowerBound = -(this.activeRequest.buyFromGridLimitW - pucGridPower); + long levlPowerUpperBound = -(this.activeRequest.sellToGridLimitW - pucGridPower); + return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } @Override public void buildJsonApiRoutes(JsonApiBuilder builder) { builder.handleRequest(METHOD, call -> { - return this.requestHandler.addRequest(call.getRequest()); + var levlControlRequest = LevlControlRequest.from(call.getRequest()); + this.nextRequest = levlControlRequest; + return JsonrpcResponseSuccess + .from(this.generateResponse(call.getRequest().getId(), levlControlRequest.getLevlRequestId())); }); } + private JsonObject generateResponse(UUID requestId, String levlRequestId) { + JsonObject response = new JsonObject(); + var result = new JsonObject(); + result.addProperty("levlRequestId", levlRequestId); + response.addProperty("id", requestId.toString()); + response.add("result", result); + return response; + } + + private static boolean isActive(LevlControlRequest request) { + LocalDateTime now = LocalDateTime.now(clock); + return !(request == null || now.isBefore(request.getStart()) || now.isAfter(request.getDeadline())); + } + + @Override + public void handleEvent(Event event) { + switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS -> { + // test cases: + // - currentRequest: aktiv; nextRequest: nicht aktiv + // - currentRequest: aktiv; nextRequest: null + if (isActive(this.nextRequest)) { + if (isActive(this.activeRequest)) { + this.realizedEnergyGridLastRequestWs = this.realizedEnergyGridWs; + this.realizedEnergyGridWs = 0; + this.realizedEnergyBatteryWs = 0; + } + this.activeRequest = this.nextRequest; + this.nextRequest = null; + } else if (!isActive(this.activeRequest)) { + this.realizedEnergyGridLastRequestWs = this.realizedEnergyGridWs; + this.realizedEnergyGridWs = 0; + this.realizedEnergyBatteryWs = 0; + this.activeRequest = null; + } + } + case EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE -> { + int levlPower = 0; + if (this.ess.getActivePower().isDefined()) { + levlPower = this.ess.getActivePower().get() - this.pucBatteryPower; + } + long levlDischargeEnergyWs = levlPower * this.cycle.getCycleTime() / 1000; + this.realizedEnergyGridWs += levlDischargeEnergyWs; + this.realizedEnergyBatteryWs += Efficiency.apply(realizedEnergyGridWs, this.activeRequest.efficiencyPercent); + this.levlSocWs -= Efficiency.apply(realizedEnergyGridWs, this.activeRequest.efficiencyPercent); + } + } + } } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index 2cb007fa762..765c1388291 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -5,22 +5,25 @@ import io.openems.common.jsonrpc.base.JsonrpcRequest; import io.openems.edge.levl.controller.common.Limit; +import java.time.Clock; import java.math.BigDecimal; import java.time.LocalDateTime; public class LevlControlRequest extends JsonrpcRequest { + protected Clock clock = Clock.systemDefaultZone(); + public static final String METHOD = "sendLevlControlRequest"; - private int sellToGridLimitW; - private int buyFromGridLimitW; + public int sellToGridLimitW; + public int buyFromGridLimitW; private String levlRequestId; - private String levlRequestTimestamp; - private int levlPowerW; - private int levlChargeDelaySec; - private int levlChargeDurationSec; + private String timestamp; + public long energyWs; + private LocalDateTime start; + private LocalDateTime deadline; private int totalRealizedDischargeEnergyWh; - private int levlSocLowerBoundPercent; - private int levlSocUpperBoundPercent; - private BigDecimal efficiencyPercent; + public int socLowerBoundPercent; + public int socUpperBoundPercent; + public double efficiencyPercent; private boolean influenceSellToGrid; private final JsonObject params; @@ -52,10 +55,10 @@ public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedExc } catch (NumberFormatException e) { throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("wrong field type in request: " + e.getMessage()); } - if (this.efficiencyPercent.compareTo(BigDecimal.ZERO) <= 0) { + if (this.efficiencyPercent < 0) { throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be > 0"); } - if (this.efficiencyPercent.compareTo(BigDecimal.valueOf(100)) > 0) { + if (this.efficiencyPercent > 0) { throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be <= 100"); } } @@ -67,16 +70,15 @@ public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedExc */ private void parseFields(JsonObject params) { this.levlRequestId = params.get("levlRequestId").getAsString(); - this.levlRequestTimestamp = params.get("levlRequestTimestamp").getAsString(); - this.levlPowerW = params.get("levlPowerW").getAsInt(); - this.levlChargeDelaySec = params.get("levlChargeDelaySec").getAsInt(); - this.levlChargeDurationSec = params.get("levlChargeDurationSec").getAsInt(); + this.energyWs = params.get("levlEnergyWs").getAsLong(); + this.start = LocalDateTime.now(this.clock).plusSeconds(params.get("levlChargeDelaySec").getAsInt()); + this.deadline = this.start.plusSeconds(params.get("levlChargeDurationSec").getAsInt()); this.totalRealizedDischargeEnergyWh = -params.get("levlSocWh").getAsInt(); - this.levlSocLowerBoundPercent = params.get("levlSocLowerBoundPercent").getAsInt(); - this.levlSocUpperBoundPercent = params.get("levlSocUpperBoundPercent").getAsInt(); + this.socLowerBoundPercent = params.get("levlSocLowerBoundPercent").getAsInt(); + this.socUpperBoundPercent = params.get("levlSocUpperBoundPercent").getAsInt(); this.sellToGridLimitW = params.get("sellToGridLimitW").getAsInt(); this.buyFromGridLimitW = params.get("buyFromGridLimitW").getAsInt(); - this.efficiencyPercent = params.get("efficiencyPercent").getAsBigDecimal(); + this.efficiencyPercent = params.get("efficiencyPercent").getAsDouble(); this.influenceSellToGrid = params.has("influenceSellToGrid") ? params.get("influenceSellToGrid").getAsBoolean() : true; @@ -95,6 +97,14 @@ public String getLevlRequestId() { return this.levlRequestId; } + protected LocalDateTime getStart() { + return this.start; + } + + protected LocalDateTime getDeadline() { + return this.deadline; + } + @Override public JsonObject getParams() { return this.params; diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java index 0a6fc9a443e..b31517bbaa4 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java @@ -13,15 +13,15 @@ public class RequestHandler { - private List requests; + private List requests; public RequestHandler() { this.requests = new ArrayList<>(); } - + public JsonrpcResponse addRequest(JsonrpcRequest request) throws OpenemsNamedException { var levlControlRequest = LevlControlRequest.from(request); - this.requests.add(request); + this.requests.add(levlControlRequest); return JsonrpcResponseSuccess .from(this.generateResponse(request.getId(), levlControlRequest.getLevlRequestId())); } @@ -35,8 +35,13 @@ private JsonObject generateResponse(UUID requestId, String levlRequestId) { return response; } + /** + * Returns the active request. + * + * @return the active request. Null if no request is active. + */ // TODO implement me - protected JsonrpcRequest getActiveRequest() { + protected LevlControlRequest getActiveRequest() { return this.requests.get(0); } From 9c18e6633387b461f4859159bd9494f5b12fae3b Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Wed, 23 Oct 2024 12:23:57 +0200 Subject: [PATCH 03/41] levl-1290/1295: add handling for influenceSellToGrid and update levlSoc; remove physical soc bounds and target grid setpoint from config --- .../openems/edge/levl/controller/Config.java | 11 - .../controller/ControllerEssBalancing.java | 9 +- .../ControllerEssBalancingImpl.java | 236 ++++++++++-------- .../levl/controller/LevlControlRequest.java | 11 +- 4 files changed, 139 insertions(+), 128 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java index 0370fff5093..cda77150356 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java @@ -23,9 +23,6 @@ @AttributeDefinition(name = "Grid-Meter-ID", description = "ID of the Grid-Meter.") String meter_id(); - @AttributeDefinition(name = "Target Grid Setpoint", description = "The target setpoint for grid. Positive for buy-from-grid; negative for sell-to-grid.") - int targetGridSetpoint() default 0; - @AttributeDefinition(name = "Ess target filter", description = "This is auto-generated by 'Ess-ID'.") String ess_target() default "(enabled=true)"; @@ -33,12 +30,4 @@ String meter_target() default "(enabled=true)"; String webconsole_configurationFactory_nameHint() default "Controller Ess Balancing [{id}]"; - - @AttributeDefinition(name = "Physical-SoC-Lower-Bound-Percent", description = "Physical lower SoC Bound (%) of the battery") - int physical_soc_lower_bound_percent() default 0; - - @AttributeDefinition(name = "Physical-SoC-Upper-Bound-Percent", description = "Physical upper SoC Bound (%) of the battery") - int physical_soc_upper_bound_percent() default 100; - - } \ No newline at end of file diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java index 057804e40ca..8a99c63cd49 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -5,6 +5,7 @@ import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.channel.IntegerReadChannel; +import io.openems.edge.common.channel.LongReadChannel; import io.openems.edge.common.channel.StringReadChannel; import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; @@ -13,7 +14,7 @@ public interface ControllerEssBalancing extends Controller, OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - REALIZED_DISCHARGE_POWER_W(Doc.of(OpenemsType.INTEGER) // + REALIZED_DISCHARGE_POWER_W(Doc.of(OpenemsType.LONG) // .unit(Unit.WATT) // .persistencePriority(PersistencePriority.HIGH) .text("the cumulated amount of discharge power that was realized since the last discharge request (in W)")), // @@ -37,7 +38,7 @@ public Doc doc() { * Returns the IntegerReadChannel for the realized power. * @return the IntegerReadChannel */ - public default IntegerReadChannel getRealizedPowerWChannel() { + public default LongReadChannel getRealizedPowerWChannel() { return this.channel(ChannelId.REALIZED_DISCHARGE_POWER_W); } @@ -45,7 +46,7 @@ public default IntegerReadChannel getRealizedPowerWChannel() { * Returns the value of the realized power. * @return the value of the realized power */ - public default Value getRealizedPowerW() { + public default Value getRealizedPowerW() { return this.getRealizedPowerWChannel().value(); } @@ -53,7 +54,7 @@ public default Value getRealizedPowerW() { * Sets the next value of the realized power. * @param value the next value */ - public default void _setRealizedPowerW(Integer value) { + public default void _setRealizedPowerW(Long value) { this.getRealizedPowerWChannel().setNextValue(value); } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 853f32bf50d..603b2922619 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -22,7 +22,6 @@ import io.openems.common.exceptions.InvalidValueException; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.jsonrpc.base.JsonrpcRequest; import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; @@ -37,21 +36,13 @@ import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.levl.controller.common.Efficiency; - @Designate(ocd = Config.class, factory = true) -@Component( - name = "Levl.Controller.Symmetric.Balancing", - immediate = true, - configurationPolicy = ConfigurationPolicy.REQUIRE -) -@EventTopics({ - EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, - EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, -}) +@Component(name = "Levl.Controller.Symmetric.Balancing", immediate = true, configurationPolicy = ConfigurationPolicy.REQUIRE) +@EventTopics({ EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, }) -//TODO: Channel erstellen um realizedValue des letzten Zyklus an Levl zu geben -public class ControllerEssBalancingImpl extends AbstractOpenemsComponent implements Controller, OpenemsComponent, ComponentJsonApi, EventHandler { +public class ControllerEssBalancingImpl extends AbstractOpenemsComponent + implements ControllerEssBalancing, ComponentJsonApi, EventHandler { private final Logger log = LoggerFactory.getLogger(ControllerEssBalancingImpl.class); @@ -67,22 +58,20 @@ public class ControllerEssBalancingImpl extends AbstractOpenemsComponent impleme @Reference private Cycle cycle; - private Config config; - + private static Clock clock = Clock.systemDefaultZone(); - - private LevlControlRequest activeRequest; + + private LevlControlRequest currentRequest; private LevlControlRequest nextRequest; - + private RequestHandler requestHandler; private long levlSocWs; private long realizedEnergyBatteryWs; private long realizedEnergyGridWs; - private long realizedEnergyGridLastRequestWs; - + private int pucBatteryPower; - + private static final String METHOD = "sendLevlControlRequest"; public ControllerEssBalancingImpl() { @@ -131,7 +120,7 @@ public void run() throws OpenemsNamedException { /* * Calculates required charge/discharge power */ - var calculatedPower = calculateRequiredPower(this.config.targetGridSetpoint()); + var calculatedPower = calculateRequiredPower(); /* * set result @@ -148,11 +137,11 @@ public void run() throws OpenemsNamedException { * @param gridPower the buy-from-grid/sell-to grid power * @param targetGridSetpoint the configured targetGridSetpoint * @return the required power - * @throws InvalidValueException + * @throws InvalidValueException */ - protected int calculateRequiredPower(int targetGridSetpoint) throws OpenemsNamedException { - float cycleTimeSec = this.cycle.getCycleTime() / 1000; - + protected int calculateRequiredPower() throws OpenemsNamedException { + double cycleTimeS = this.cycle.getCycleTime() / 1000; + // load physical values int physicalSoc = this.ess.getSoc().getOrError(); int gridPower = this.meter.getActivePower().getOrError(); @@ -160,21 +149,22 @@ protected int calculateRequiredPower(int targetGridSetpoint) throws OpenemsNamed int essCapacity = this.ess.getCapacity().getOrError(); int minEssPower = this.ess.getPower().getMinPower(this.ess, Phase.ALL, Pwr.ACTIVE); int maxEssPower = this.ess.getPower().getMaxPower(this.ess, Phase.ALL, Pwr.ACTIVE); - + long essCapacityWs = essCapacity * 3600; long physicalSocWs = physicalSoc / 100 * essCapacityWs; - - - // ##### primary use case (puc) calculation - long pucSocWs = physicalSocWs - this.levlSocWs; - this.pucBatteryPower = calculatePucBatteryPower(targetGridSetpoint, cycleTimeSec, gridPower, essPower, essCapacityWs, pucSocWs, minEssPower, maxEssPower); + + // primary use case (puc) calculation + long pucSocWs = physicalSocWs - this.levlSocWs; + this.pucBatteryPower = calculatePucBatteryPower(cycleTimeS, gridPower, essPower, + essCapacityWs, pucSocWs, minEssPower, maxEssPower); int pucGridPower = gridPower - essPower + this.pucBatteryPower; - long nextPucSocWs = pucSocWs + Math.round(this.pucBatteryPower * cycleTimeSec); - - //##### Levl calculation - int levlPowerW = this.calculateLevlPowerW(this.pucBatteryPower, minEssPower, maxEssPower, pucGridPower, nextPucSocWs, essCapacityWs, cycleTimeSec); - - //##### Overall calculation + long nextPucSocWs = pucSocWs + Math.round(this.pucBatteryPower * cycleTimeS); + + // levl calculation + int levlPowerW = this.calculateLevlPowerW(this.pucBatteryPower, minEssPower, maxEssPower, pucGridPower, + nextPucSocWs, essCapacityWs, cycleTimeS); + + // overall calculation long batteryPowerW = this.pucBatteryPower + levlPowerW; return (int) batteryPowerW; } @@ -182,8 +172,7 @@ protected int calculateRequiredPower(int targetGridSetpoint) throws OpenemsNamed /** * Calculates the power of the primary use case (puc) * - * @param targetGridSetpoint - * @param cycleTimeSec + * @param cycleTimeS * @param gridPower * @param essPower * @param essCapacityWs @@ -192,21 +181,22 @@ protected int calculateRequiredPower(int targetGridSetpoint) throws OpenemsNamed * @param maxEssPower * @return */ - private int calculatePucBatteryPower(int targetGridSetpoint, float cycleTimeSec, int gridPower, int essPower, + private int calculatePucBatteryPower(double cycleTimeS, int gridPower, int essPower, long essCapacityWs, long pucSocWs, int minEssPower, int maxEssPower) { // calculate pucPower without any limits - int pucBatteryPower = gridPower + essPower - targetGridSetpoint; - + int pucBatteryPower = gridPower + essPower; + // apply ess power limits pucBatteryPower = Math.max(Math.min(pucBatteryPower, maxEssPower), minEssPower); - + // apply soc bounds - pucBatteryPower = applyPucSocBounds(cycleTimeSec, essCapacityWs, pucSocWs, pucBatteryPower); + pucBatteryPower = applyPucSocBounds(cycleTimeS, essCapacityWs, pucSocWs, pucBatteryPower); return pucBatteryPower; } /** - * Checks and corrects the pucPower if it would exceed the upper or lower limits of the SoC. + * Checks and corrects the pucPower if it would exceed the upper or lower limits + * of the SoC. * * @param cycleTimeSec * @param essCapacityWs @@ -214,67 +204,87 @@ private int calculatePucBatteryPower(int targetGridSetpoint, float cycleTimeSec, * @param pucPower * @return the restricted pucPower */ - private int applyPucSocBounds(float cycleTimeS, long essCapacityWs, long pucSocWs, int pucPower) { - //TODO: fix casting - //TODO: efficiency in if anwenden - if (pucSocWs - (pucPower * cycleTimeS) > essCapacityWs) { - pucPower = (int) -((essCapacityWs - pucSocWs) / cycleTimeS); - pucPower = (int) Efficiency.apply(Math.round(pucPower / cycleTimeS), this.activeRequest.efficiencyPercent); - } - if (pucSocWs - (pucPower * cycleTimeS) < 0) { - pucPower = (int) -((0 - pucSocWs) / cycleTimeS); - pucPower = (int) Efficiency.apply(Math.round(pucPower / cycleTimeS), this.activeRequest.efficiencyPercent); - } - return pucPower; + private int applyPucSocBounds(double cycleTimeS, long essCapacityWs, long pucSocWs, int pucPower) { + long dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; + long dischargeEnergyUpperBoundWs = pucSocWs; + + long powerLowerBound = Efficiency.apply(Math.round(dischargeEnergyLowerBoundWs / cycleTimeS), + this.currentRequest.efficiencyPercent); + long powerUpperBound = Efficiency.apply(Math.round(dischargeEnergyUpperBoundWs / cycleTimeS), + this.currentRequest.efficiencyPercent); + + return (int) Math.max(Math.min(pucPower, powerUpperBound), powerLowerBound); } - - private int calculateLevlPowerW(int pucBatteryPower, int minEssPower, int maxEssPower, int pucGridPower, long nextPucSocWs, long essCapacityWs, float cycleTimeS) { - long levlPower = Math.round((this.activeRequest.energyWs - this.realizedEnergyGridWs) / (double) cycleTimeS); - + + private int calculateLevlPowerW(int pucBatteryPower, int minEssPower, int maxEssPower, int pucGridPower, + long nextPucSocWs, long essCapacityWs, double cycleTimeS) { + long levlPower = Math.round((this.currentRequest.energyWs - this.realizedEnergyGridWs) / (double) cycleTimeS); + levlPower = this.applyBatteryPowerLimitsToLevlPower(levlPower, pucBatteryPower, minEssPower, maxEssPower); levlPower = this.applySocBoundariesToLevlPower(levlPower, nextPucSocWs, essCapacityWs, cycleTimeS); levlPower = this.applyGridPowerLimitsToLevlPower(levlPower, pucGridPower); - + levlPower = this.applyInfluenceSellToGridConstraint(levlPower, pucGridPower); + return (int) levlPower; } - - private long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, int maxEssPower) { + + private long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower) { + if (!this.currentRequest.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 = Math.min(levlPower, pucGridPower); + } + } + return levlPower; + } + + private long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, + int maxEssPower) { int levlPowerLowerBound = minEssPower - pucBatteryPower; int levlPowerUpperBound = maxEssPower - pucBatteryPower; return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } - - private long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long essCapacityWs, float cycleTimeS) { - long levlSocLowerBoundWs = this.activeRequest.socLowerBoundPercent / 100 * essCapacityWs - nextPucSocWs; - long levlSocUpperBoundWs = this.activeRequest.socUpperBoundPercent / 100 * essCapacityWs - nextPucSocWs; - if (levlSocLowerBoundWs > 0) levlSocLowerBoundWs = 0; - if (levlSocUpperBoundWs < 0) levlSocUpperBoundWs = 0; - + + private long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long essCapacityWs, + double cycleTimeS) { + long levlSocLowerBoundWs = this.currentRequest.socLowerBoundPercent / 100 * essCapacityWs - nextPucSocWs; + long levlSocUpperBoundWs = this.currentRequest.socUpperBoundPercent / 100 * essCapacityWs - nextPucSocWs; + if (levlSocLowerBoundWs > 0) + levlSocLowerBoundWs = 0; + if (levlSocUpperBoundWs < 0) + levlSocUpperBoundWs = 0; + long levlDischargeEnergyLowerBoundWs = -(levlSocUpperBoundWs - this.levlSocWs); long levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - this.levlSocWs); - - long levlPowerLowerBound = Efficiency.apply(Math.round(levlDischargeEnergyLowerBoundWs / cycleTimeS), this.activeRequest.efficiencyPercent); - long levlPowerUpperBound = Efficiency.apply(Math.round(levlDischargeEnergyUpperBoundWs / cycleTimeS), this.activeRequest.efficiencyPercent); - + + long levlPowerLowerBound = Efficiency.apply(Math.round(levlDischargeEnergyLowerBoundWs / cycleTimeS), + this.currentRequest.efficiencyPercent); + long levlPowerUpperBound = Efficiency.apply(Math.round(levlDischargeEnergyUpperBoundWs / cycleTimeS), + this.currentRequest.efficiencyPercent); + return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } - + private long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower) { - long levlPowerLowerBound = -(this.activeRequest.buyFromGridLimitW - pucGridPower); - long levlPowerUpperBound = -(this.activeRequest.sellToGridLimitW - pucGridPower); + long levlPowerLowerBound = -(this.currentRequest.buyFromGridLimitW - pucGridPower); + long levlPowerUpperBound = -(this.currentRequest.sellToGridLimitW - pucGridPower); return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } - + @Override public void buildJsonApiRoutes(JsonApiBuilder builder) { builder.handleRequest(METHOD, call -> { var levlControlRequest = LevlControlRequest.from(call.getRequest()); this.nextRequest = levlControlRequest; + this.levlSocWs = levlControlRequest.levlSocWh * 3600 - this.realizedEnergyBatteryWs; return JsonrpcResponseSuccess - .from(this.generateResponse(call.getRequest().getId(), levlControlRequest.getLevlRequestId())); + .from(this.generateResponse(call.getRequest().getId(), levlControlRequest.getLevlRequestId())); }); } - + private JsonObject generateResponse(UUID requestId, String levlRequestId) { JsonObject response = new JsonObject(); var result = new JsonObject(); @@ -283,44 +293,50 @@ private JsonObject generateResponse(UUID requestId, String levlRequestId) { response.add("result", result); return response; } - + private static boolean isActive(LevlControlRequest request) { LocalDateTime now = LocalDateTime.now(clock); return !(request == null || now.isBefore(request.getStart()) || now.isAfter(request.getDeadline())); } - + @Override public void handleEvent(Event event) { switch (event.getTopic()) { - case EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS -> { - // test cases: - // - currentRequest: aktiv; nextRequest: nicht aktiv - // - currentRequest: aktiv; nextRequest: null - if (isActive(this.nextRequest)) { - if (isActive(this.activeRequest)) { - this.realizedEnergyGridLastRequestWs = this.realizedEnergyGridWs; - this.realizedEnergyGridWs = 0; - this.realizedEnergyBatteryWs = 0; - } - this.activeRequest = this.nextRequest; - this.nextRequest = null; - } else if (!isActive(this.activeRequest)) { - this.realizedEnergyGridLastRequestWs = this.realizedEnergyGridWs; - this.realizedEnergyGridWs = 0; - this.realizedEnergyBatteryWs = 0; - this.activeRequest = null; + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS -> { + // test cases: + // - currentRequest: aktiv; nextRequest: nicht aktiv + // - currentRequest: aktiv; nextRequest: null + // ... + if (isActive(this.nextRequest)) { + if (this.currentRequest != null) { + this.finishRequest(); } + this.currentRequest = this.nextRequest; + this.nextRequest = null; + } else if (!isActive(this.currentRequest)) { + this.finishRequest(); } - case EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE -> { - int levlPower = 0; - if (this.ess.getActivePower().isDefined()) { - levlPower = this.ess.getActivePower().get() - this.pucBatteryPower; - } - long levlDischargeEnergyWs = levlPower * this.cycle.getCycleTime() / 1000; - this.realizedEnergyGridWs += levlDischargeEnergyWs; - this.realizedEnergyBatteryWs += Efficiency.apply(realizedEnergyGridWs, this.activeRequest.efficiencyPercent); - this.levlSocWs -= Efficiency.apply(realizedEnergyGridWs, this.activeRequest.efficiencyPercent); + } + case EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE -> { + int levlPower = 0; + if (this.ess.getActivePower().isDefined()) { + levlPower = this.ess.getActivePower().get() - this.pucBatteryPower; } + long levlDischargeEnergyWs = levlPower * this.cycle.getCycleTime() / 1000; + this.realizedEnergyGridWs += levlDischargeEnergyWs; + this.realizedEnergyBatteryWs += Efficiency.apply(realizedEnergyGridWs, + this.currentRequest.efficiencyPercent); + this.levlSocWs -= Efficiency.apply(realizedEnergyGridWs, this.currentRequest.efficiencyPercent); + } } } + + private void finishRequest() { + // Channel realizedEnergy und requestTimestamp schreiben + this._setRealizedPowerW(this.realizedEnergyGridWs); + this._setLastControlRequestTimestamp(this.currentRequest.getTimestamp()); + this.realizedEnergyGridWs = 0; + this.realizedEnergyBatteryWs = 0; + this.currentRequest = null; + } } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index 765c1388291..ac0ffc5b67c 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -20,11 +20,11 @@ public class LevlControlRequest extends JsonrpcRequest { public long energyWs; private LocalDateTime start; private LocalDateTime deadline; - private int totalRealizedDischargeEnergyWh; + public int levlSocWh; public int socLowerBoundPercent; public int socUpperBoundPercent; public double efficiencyPercent; - private boolean influenceSellToGrid; + public boolean influenceSellToGrid; private final JsonObject params; /** @@ -70,10 +70,11 @@ public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedExc */ private void parseFields(JsonObject params) { this.levlRequestId = params.get("levlRequestId").getAsString(); + this.timestamp = params.get("levlRequestTimestamp").getAsString(); this.energyWs = params.get("levlEnergyWs").getAsLong(); this.start = LocalDateTime.now(this.clock).plusSeconds(params.get("levlChargeDelaySec").getAsInt()); this.deadline = this.start.plusSeconds(params.get("levlChargeDurationSec").getAsInt()); - this.totalRealizedDischargeEnergyWh = -params.get("levlSocWh").getAsInt(); + this.levlSocWh = params.get("levlSocWh").getAsInt(); this.socLowerBoundPercent = params.get("levlSocLowerBoundPercent").getAsInt(); this.socUpperBoundPercent = params.get("levlSocUpperBoundPercent").getAsInt(); this.sellToGridLimitW = params.get("sellToGridLimitW").getAsInt(); @@ -97,6 +98,10 @@ public String getLevlRequestId() { return this.levlRequestId; } + public String getTimestamp() { + return this.timestamp; + } + protected LocalDateTime getStart() { return this.start; } From 2f23cc4817708fdd7e4e2cb8479fb380b52c4655 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Thu, 24 Oct 2024 14:44:59 +0200 Subject: [PATCH 04/41] levl-1290/1296: add unit tests and fix related bugs --- .../ControllerEssBalancingImpl.java | 90 ++--- .../levl/controller/LevlControlRequest.java | 10 +- .../ess/balancing/BalancingImplTest.java | 97 ----- .../levl/controller/BalancingImplTest.java | 99 ++++++ .../ControllerEssBalancingImplTest.java | 334 ++++++++++++++++++ .../controller}/MyConfig.java | 7 +- 6 files changed, 487 insertions(+), 150 deletions(-) delete mode 100644 io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/BalancingImplTest.java create mode 100644 io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/BalancingImplTest.java create mode 100644 io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/ControllerEssBalancingImplTest.java rename io.openems.edge.levl.controller/test/io/openems/edge/{controller/ess/balancing => levl/controller}/MyConfig.java (91%) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 603b2922619..9f08c701140 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -50,27 +50,26 @@ public class ControllerEssBalancingImpl extends AbstractOpenemsComponent private ConfigurationAdmin cm; @Reference - private ManagedSymmetricEss ess; + protected ManagedSymmetricEss ess; @Reference - private ElectricityMeter meter; + protected ElectricityMeter meter; @Reference - private Cycle cycle; + protected Cycle cycle; private Config config; - private static Clock clock = Clock.systemDefaultZone(); + protected static Clock clock = Clock.systemDefaultZone(); - private LevlControlRequest currentRequest; - private LevlControlRequest nextRequest; + protected LevlControlRequest currentRequest; + protected LevlControlRequest nextRequest; - private RequestHandler requestHandler; - private long levlSocWs; - private long realizedEnergyBatteryWs; - private long realizedEnergyGridWs; - - private int pucBatteryPower; + //private RequestHandler requestHandler; + protected long levlSocWs; + protected int pucBatteryPower; + protected long realizedEnergyGridWs; + protected long realizedEnergyBatteryWs; private static final String METHOD = "sendLevlControlRequest"; @@ -140,24 +139,29 @@ public void run() throws OpenemsNamedException { * @throws InvalidValueException */ protected int calculateRequiredPower() throws OpenemsNamedException { - double cycleTimeS = this.cycle.getCycleTime() / 1000; + double cycleTimeS = this.cycle.getCycleTime() / 1000.0; // load physical values int physicalSoc = this.ess.getSoc().getOrError(); int gridPower = this.meter.getActivePower().getOrError(); int essPower = this.ess.getActivePower().getOrError(); int essCapacity = this.ess.getCapacity().getOrError(); + + //TODO: wäre das nicht der richtige Wert? G +// int minEssPower = this.ess.getAllowedChargePower().getOrError(); +// int maxEssPower = this.ess.getAllowedDischargePower().getOrError(); + int minEssPower = this.ess.getPower().getMinPower(this.ess, Phase.ALL, Pwr.ACTIVE); int maxEssPower = this.ess.getPower().getMaxPower(this.ess, Phase.ALL, Pwr.ACTIVE); long essCapacityWs = essCapacity * 3600; - long physicalSocWs = physicalSoc / 100 * essCapacityWs; + long physicalSocWs = Math.round((physicalSoc / 100.0) * essCapacityWs); // primary use case (puc) calculation long pucSocWs = physicalSocWs - this.levlSocWs; this.pucBatteryPower = calculatePucBatteryPower(cycleTimeS, gridPower, essPower, essCapacityWs, pucSocWs, minEssPower, maxEssPower); - int pucGridPower = gridPower - essPower + this.pucBatteryPower; + int pucGridPower = gridPower + essPower - this.pucBatteryPower; long nextPucSocWs = pucSocWs + Math.round(this.pucBatteryPower * cycleTimeS); // levl calculation @@ -181,7 +185,7 @@ protected int calculateRequiredPower() throws OpenemsNamedException { * @param maxEssPower * @return */ - private int calculatePucBatteryPower(double cycleTimeS, int gridPower, int essPower, + protected int calculatePucBatteryPower(double cycleTimeS, int gridPower, int essPower, long essCapacityWs, long pucSocWs, int minEssPower, int maxEssPower) { // calculate pucPower without any limits int pucBatteryPower = gridPower + essPower; @@ -204,7 +208,7 @@ private int calculatePucBatteryPower(double cycleTimeS, int gridPower, int essPo * @param pucPower * @return the restricted pucPower */ - private int applyPucSocBounds(double cycleTimeS, long essCapacityWs, long pucSocWs, int pucPower) { + protected int applyPucSocBounds(double cycleTimeS, long essCapacityWs, long pucSocWs, int pucPower) { long dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; long dischargeEnergyUpperBoundWs = pucSocWs; @@ -228,30 +232,17 @@ private int calculateLevlPowerW(int pucBatteryPower, int minEssPower, int maxEss return (int) levlPower; } - private long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower) { - if (!this.currentRequest.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 = Math.min(levlPower, pucGridPower); - } - } - return levlPower; - } - - private long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, + protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, int maxEssPower) { int levlPowerLowerBound = minEssPower - pucBatteryPower; int levlPowerUpperBound = maxEssPower - pucBatteryPower; return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } - private long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long essCapacityWs, + protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long essCapacityWs, double cycleTimeS) { - long levlSocLowerBoundWs = this.currentRequest.socLowerBoundPercent / 100 * essCapacityWs - nextPucSocWs; - long levlSocUpperBoundWs = this.currentRequest.socUpperBoundPercent / 100 * essCapacityWs - nextPucSocWs; + long levlSocLowerBoundWs = Math.round(this.currentRequest.socLowerBoundPercent / 100.0 * essCapacityWs) - nextPucSocWs; + long levlSocUpperBoundWs = Math.round(this.currentRequest.socUpperBoundPercent / 100.0 * essCapacityWs) - nextPucSocWs; if (levlSocLowerBoundWs > 0) levlSocLowerBoundWs = 0; if (levlSocUpperBoundWs < 0) @@ -260,20 +251,33 @@ private long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, lo long levlDischargeEnergyLowerBoundWs = -(levlSocUpperBoundWs - this.levlSocWs); long levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - this.levlSocWs); - long levlPowerLowerBound = Efficiency.apply(Math.round(levlDischargeEnergyLowerBoundWs / cycleTimeS), + long levlPowerLowerBound = Efficiency.unapply(Math.round(levlDischargeEnergyLowerBoundWs / cycleTimeS), this.currentRequest.efficiencyPercent); - long levlPowerUpperBound = Efficiency.apply(Math.round(levlDischargeEnergyUpperBoundWs / cycleTimeS), + long levlPowerUpperBound = Efficiency.unapply(Math.round(levlDischargeEnergyUpperBoundWs / cycleTimeS), this.currentRequest.efficiencyPercent); return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } - private long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower) { + protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower) { long levlPowerLowerBound = -(this.currentRequest.buyFromGridLimitW - pucGridPower); long levlPowerUpperBound = -(this.currentRequest.sellToGridLimitW - pucGridPower); return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } + public long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower) { + if (!this.currentRequest.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 = Math.min(levlPower, pucGridPower); + } + } + return levlPower; + } + @Override public void buildJsonApiRoutes(JsonApiBuilder builder) { builder.handleRequest(METHOD, call -> { @@ -303,17 +307,13 @@ private static boolean isActive(LevlControlRequest request) { public void handleEvent(Event event) { switch (event.getTopic()) { case EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS -> { - // test cases: - // - currentRequest: aktiv; nextRequest: nicht aktiv - // - currentRequest: aktiv; nextRequest: null - // ... if (isActive(this.nextRequest)) { if (this.currentRequest != null) { this.finishRequest(); } this.currentRequest = this.nextRequest; this.nextRequest = null; - } else if (!isActive(this.currentRequest)) { + } else if (currentRequest != null && !isActive(this.currentRequest)) { this.finishRequest(); } } @@ -322,11 +322,11 @@ public void handleEvent(Event event) { if (this.ess.getActivePower().isDefined()) { levlPower = this.ess.getActivePower().get() - this.pucBatteryPower; } - long levlDischargeEnergyWs = levlPower * this.cycle.getCycleTime() / 1000; + long levlDischargeEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); this.realizedEnergyGridWs += levlDischargeEnergyWs; - this.realizedEnergyBatteryWs += Efficiency.apply(realizedEnergyGridWs, + this.realizedEnergyBatteryWs += Efficiency.apply(levlDischargeEnergyWs, this.currentRequest.efficiencyPercent); - this.levlSocWs -= Efficiency.apply(realizedEnergyGridWs, this.currentRequest.efficiencyPercent); + this.levlSocWs -= Efficiency.apply(levlDischargeEnergyWs, this.currentRequest.efficiencyPercent); } } } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index ac0ffc5b67c..8986d135695 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -18,8 +18,8 @@ public class LevlControlRequest extends JsonrpcRequest { private String levlRequestId; private String timestamp; public long energyWs; - private LocalDateTime start; - private LocalDateTime deadline; + public LocalDateTime start; + public LocalDateTime deadline; public int levlSocWh; public int socLowerBoundPercent; public int socUpperBoundPercent; @@ -27,6 +27,7 @@ public class LevlControlRequest extends JsonrpcRequest { public boolean influenceSellToGrid; private final JsonObject params; + /** * Creates a new LevlControlRequest from a JsonrpcRequest. * @@ -39,6 +40,11 @@ public static LevlControlRequest from(JsonrpcRequest request) throws OpenemsErro return new LevlControlRequest(params); } + public LevlControlRequest() { + super(LevlControlRequest.METHOD); + this.params = new JsonObject(); + } + /** * Creates a new LevlControlRequest from a JsonObject. * diff --git a/io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/BalancingImplTest.java b/io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/BalancingImplTest.java deleted file mode 100644 index 56301add84b..00000000000 --- a/io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/BalancingImplTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package io.openems.edge.controller.ess.balancing; - -import org.junit.Test; - -import io.openems.common.types.ChannelAddress; -import io.openems.edge.common.test.AbstractComponentTest.TestCase; -import io.openems.edge.common.test.DummyConfigurationAdmin; -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.levl.controller.ControllerEssBalancingImpl; -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_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, - "SetActivePowerEquals"); - - private static final String METER_ID = "meter0"; - private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); - - @Test - public void test() throws Exception { - new ControllerTest(new ControllerEssBalancingImpl()) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // - .setPower(new DummyPower(0.3, 0.3, 0.1))) // - .addReference("meter", new DummyElectricityMeter(METER_ID)) // - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // - .setTargetGridSetpoint(0) // - .build()) - .next(new TestCase() // - .input(ESS_ACTIVE_POWER, 0) // - .input(METER_ACTIVE_POWER, 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)); - } - -} 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..2bb54f17530 --- /dev/null +++ b/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/BalancingImplTest.java @@ -0,0 +1,99 @@ +package io.openems.edge.levl.controller; + +import org.junit.Test; + +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.DummyConfigurationAdmin; +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.levl.controller.ControllerEssBalancingImpl; +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_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, +// "SetActivePowerEquals"); +// +// private static final String METER_ID = "meter0"; +// private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); +// +// @Test +// public void test() throws Exception { +// new ControllerTest(new ControllerEssBalancingImpl()) // +// .addReference("cm", new DummyConfigurationAdmin()) // +// .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // +// .setPower(new DummyPower(0.3, 0.3, 0.1))) // +// .addReference("meter", new DummyElectricityMeter(METER_ID)) // +// .activate(MyConfig.create() // +// .setId(CTRL_ID) // +// .setEssId(ESS_ID) // +// .setMeterId(METER_ID) // +// .setTargetGridSetpoint(0) // +// .build()) +// .next(new TestCase() // +// .input(ESS_ACTIVE_POWER, 0) // +// .input(METER_ACTIVE_POWER, 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)); +// } + +} 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..c2efd7857b6 --- /dev/null +++ b/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/ControllerEssBalancingImplTest.java @@ -0,0 +1,334 @@ +package io.openems.edge.levl.controller; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.osgi.service.event.Event; + +import io.openems.common.event.EventBuilderTest; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.event.EdgeEventConstants; +import io.openems.edge.common.test.DummyCycle; +import io.openems.edge.common.test.DummyEventAdmin; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.ess.test.DummyPower; +import io.openems.edge.meter.test.DummyElectricityMeter; + +public class ControllerEssBalancingImplTest { + + /* +public class CalculateSocTest { + + @Test + public void testEmpty() { + var esss = List.of(); + assertNull(new CalculateSoc().add(esss).calculate()); + } + + @Test + public void testNull() { + var esss = List.of(// + new DummySymmetricEss("ess0"), // + new DummySymmetricEss("ess1")); + assertNull(new CalculateSoc().add(esss).calculate()); + } + + @Test + public void testWeightedSoc() { + var esss = List.of(// + new DummySymmetricEss("ess0").withCapacity(10_000).withSoc(40), // + new DummySymmetricEss("ess1").withCapacity(20_000).withSoc(60)); + assertEquals(53, (int) new CalculateSoc().add(esss).calculate()); + } + + @Test + public void testAverageSoc() { + var esss = List.of(// + new DummySymmetricEss("ess0").withCapacity(10_000).withSoc(40), // + new DummySymmetricEss("ess1"), // + new DummySymmetricEss("ess2").withSoc(60)); + assertEquals(50, (int) new CalculateSoc().add(esss).calculate()); + } + +} + */ + +// @InjectMocks + private ControllerEssBalancingImpl underTest; + +// @Mock +// private ControllerEssBalancingImpl mockCalculator; // Mock für die Methode + + + @Before + public void setUp() { +// MockitoAnnotations.initMocks(this); + this.underTest = new ControllerEssBalancingImpl(); + } + + @Test + public void testCalculateRequiredPower() throws OpenemsNamedException { + //TODO: Keine Tests da ess, meter, ... gemockt warden müsste. + this.underTest.cycle = new DummyCycle(1000); + this.underTest.ess = new DummyManagedSymmetricEss("ess0") + //TODO: maximale Scheinleistung! Holen wir uns damit im Code den richtigen Wert? != allowedCharge/DischargePower + //TODO: Mit diesem Setup ist min/maxPower = MAX/MIN-Int und nur buy/sellGridLimit schränken ein + .setPower(new DummyPower(0.3, 0.3, 0.1)) + .withActivePower(-100) + .withCapacity(500) // 1.800.000 Ws + .withSoc(50) // 900.000 Ws + .withAllowedChargePower(500) + .withAllowedDischargePower(500); + this.underTest.meter = new DummyElectricityMeter("meter0").withActivePower(200); + + this.underTest.realizedEnergyGridWs = 100; + this.underTest.levlSocWs = 2000; + this.underTest.currentRequest = new LevlControlRequest(); + this.underTest.currentRequest.energyWs = 200_000; + this.underTest.currentRequest.efficiencyPercent = 100; + this.underTest.currentRequest.socLowerBoundPercent = 20; + this.underTest.currentRequest.socUpperBoundPercent = 80; + this.underTest.currentRequest.buyFromGridLimitW = 1000; + this.underTest.currentRequest.sellToGridLimitW = -1000; + this.underTest.currentRequest.influenceSellToGrid = true; + + int result = this.underTest.calculateRequiredPower(); + + Assert.assertEquals(1100, result); + } + + // Primary use case calculation + + @Test + public void testApplyPucSocBounds() { + this.underTest.currentRequest = new LevlControlRequest(); + this.underTest.currentRequest.efficiencyPercent = 100; + + Assert.assertEquals("good case discharge", 30, this.underTest.applyPucSocBounds(1, 100, 50, 30)); + Assert.assertEquals("good case charge", -30, this.underTest.applyPucSocBounds(1, 100, 50, -30)); + Assert.assertEquals("minimum limit applies", 50, this.underTest.applyPucSocBounds(1, 100, 50, 70)); + Assert.assertEquals("minimum limit applies due to cycleTime", 25, this.underTest.applyPucSocBounds(2, 100, 50, 30)); + Assert.assertEquals("maximum limit applies", -50, this.underTest.applyPucSocBounds(1, 100, 50, -70)); + Assert.assertEquals("no charging allowed because soc is 100%", 0, this.underTest.applyPucSocBounds(1, 100, 100, -20)); + Assert.assertEquals("no discharging allowed because soc is 0%", 0, this.underTest.applyPucSocBounds(1, 100, 0, 20)); + Assert.assertEquals("discharging allowed with soc 100%", 20, this.underTest.applyPucSocBounds(1, 100, 100, 20)); + Assert.assertEquals("charging allowed with soc 0%", -20, this.underTest.applyPucSocBounds(1, 100, 0, -20)); + + this.underTest.currentRequest.efficiencyPercent = 80; + //TODO: Efficiency prüfen + Assert.assertEquals("good case discharge /w efficiency", 30, this.underTest.applyPucSocBounds(1, 100, 50, 30)); + Assert.assertEquals("good case charge /w efficiency", -30, this.underTest.applyPucSocBounds(1, 100, 50, -30)); + Assert.assertEquals("minimum limit applies /w efficiency", 63, this.underTest.applyPucSocBounds(1, 100, 50, 70)); + //TODO: Ergebnis prüfen. Ist doch falsch! Wenn ich 50 reinladen kann, dann kann ich mit WK doch nicht noch weniger reinladen +// Assert.assertEquals("maximum limit applies /w efficiency", 63, this.underTest.applyPucSocBounds(1, 100, 50, -70)); + } + + @Test + public void testCalculatePucBatteryPower() { + this.underTest.currentRequest = new LevlControlRequest(); + this.underTest.currentRequest.efficiencyPercent = 100; + + Assert.assertEquals("discharge within battery limit", 70, underTest.calculatePucBatteryPower(1, 50, 20, + 1000, 500, -150, 150)); + Assert.assertEquals("discharge outside battery limit", 150, underTest.calculatePucBatteryPower(1, 200, 20, + 1000, 500, -150, 150)); + Assert.assertEquals("charge outside battery limit", -150, underTest.calculatePucBatteryPower(1, -200, -20, + 1000, 500, -150, 150)); +} + + // 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() { + this.underTest.currentRequest = new LevlControlRequest(); + this.underTest.currentRequest.socLowerBoundPercent = 20; + this.underTest.currentRequest.socUpperBoundPercent = 80; + this.underTest.currentRequest.efficiencyPercent = 90; + + Assert.assertEquals(-22, this.underTest.applySocBoundariesToLevlPower(-100, 60, 100, 1)); + Assert.assertEquals(-10, this.underTest.applySocBoundariesToLevlPower(-10, 60, 100, 1)); + Assert.assertEquals(10, this.underTest.applySocBoundariesToLevlPower(10, 60, 100, 1)); + Assert.assertEquals(36, this.underTest.applySocBoundariesToLevlPower(100, 60, 100, 1)); + } + + @Test + public void testApplyGridPowerLimitsToLevlPower() { + this.underTest.currentRequest = new LevlControlRequest(); + this.underTest.currentRequest.buyFromGridLimitW = 80; + this.underTest.currentRequest.sellToGridLimitW = -70; + + Assert.assertEquals("levlPower within limits", 50, this.underTest.applyGridPowerLimitsToLevlPower(50, 0)); + Assert.assertEquals("levlPower within limits balancing grid", 100, this.underTest.applyGridPowerLimitsToLevlPower(100, 40)); + Assert.assertEquals("levlPower constraint by sellToGridLimit", 50, this.underTest.applyGridPowerLimitsToLevlPower(100, -20)); + Assert.assertEquals("levlPower constraint by buyFromGridLimit", -60, this.underTest.applyGridPowerLimitsToLevlPower(-100, 20)); + } + + @Test + public void testInfluenceSellToGridConstraint() { + this.underTest.currentRequest = new LevlControlRequest(); + this.underTest.currentRequest.influenceSellToGrid = true; + + Assert.assertEquals("influence allowed", 50, this.underTest.applyInfluenceSellToGridConstraint(50, 0)); + + this.underTest.currentRequest.influenceSellToGrid = false; + Assert.assertEquals("buy from grid is allowed", -50, this.underTest.applyInfluenceSellToGridConstraint(-50, 20)); + Assert.assertEquals("switch gridPower /w buy from grid to sell to grid not allowed", 20, this.underTest.applyInfluenceSellToGridConstraint(50, 20)); + Assert.assertEquals("do nothing because grid power sells to grid", 0, this.underTest.applyInfluenceSellToGridConstraint(-50, -20)); + } + + @Test + public void testHandleEvent_before_currentActive() { + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, new HashMap<>()); + + // 2024-10-24T14:00:00 + Clock clock = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); + ControllerEssBalancingImpl.clock = clock; + + LevlControlRequest currentRequest = new LevlControlRequest(); + currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); + currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); + this.underTest.currentRequest = currentRequest; + + LevlControlRequest nextRequest = new LevlControlRequest(); + nextRequest.start = LocalDateTime.of(2024, 10, 24, 14, 15, 0); + nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 29, 59); + this.underTest.nextRequest = nextRequest; + + this.underTest.handleEvent(event); + + Assert.assertEquals(currentRequest, this.underTest.currentRequest); + } + + @Test + public void testHandleEvent_before_nextRequestIsActive() { + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, new HashMap<>()); + + // 2024-10-24T14:15:00 + Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); + ControllerEssBalancingImpl.clock = clock; + + LevlControlRequest currentRequest = new LevlControlRequest(); + currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); + currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); + this.underTest.currentRequest = currentRequest; + + LevlControlRequest nextRequest = new LevlControlRequest(); + nextRequest.start = LocalDateTime.of(2024, 10, 24, 14, 15, 0); + nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 29, 59); + this.underTest.nextRequest = nextRequest; + + this.underTest.realizedEnergyGridWs = 100; + this.underTest.realizedEnergyBatteryWs = 200; + + this.underTest.handleEvent(event); + + Assert.assertEquals(nextRequest, this.underTest.currentRequest); + Assert.assertNull(this.underTest.nextRequest); + Assert.assertEquals(0, this.underTest.realizedEnergyGridWs); + Assert.assertEquals(0, this.underTest.realizedEnergyBatteryWs); + } + + @Test + public void testHandleEvent_before_gapBetweenRequests() { + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, new HashMap<>()); + + // 2024-10-24T14:15:00 + Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); + ControllerEssBalancingImpl.clock = clock; + + LevlControlRequest currentRequest = new LevlControlRequest(); + currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); + currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); + this.underTest.currentRequest = currentRequest; + + LevlControlRequest nextRequest = new LevlControlRequest(); + nextRequest.start = LocalDateTime.of(2024, 10, 24, 14, 16, 0); + nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 29, 59); + this.underTest.nextRequest = nextRequest; + + this.underTest.realizedEnergyGridWs = 100; + this.underTest.realizedEnergyBatteryWs = 200; + + this.underTest.handleEvent(event); + + Assert.assertNull(this.underTest.currentRequest); + Assert.assertEquals(nextRequest, this.underTest.nextRequest); + Assert.assertEquals(0, this.underTest.realizedEnergyGridWs); + Assert.assertEquals(0, this.underTest.realizedEnergyBatteryWs); + } + + @Test + public void testHandleEvent_before_noNextRequest() { + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, new HashMap<>()); + + // 2024-10-24T14:15:00 + Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); + ControllerEssBalancingImpl.clock = clock; + + LevlControlRequest currentRequest = new LevlControlRequest(); + currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); + currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); + this.underTest.currentRequest = currentRequest; + + this.underTest.realizedEnergyGridWs = 100; + this.underTest.realizedEnergyBatteryWs = 200; + + this.underTest.handleEvent(event); + + Assert.assertNull(this.underTest.currentRequest); + Assert.assertNull(this.underTest.nextRequest); + Assert.assertEquals(0, this.underTest.realizedEnergyGridWs); + Assert.assertEquals(0, this.underTest.realizedEnergyBatteryWs); + } + + @Test + public void testHandleEvent_before_noRequests() { + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, new HashMap<>()); + + // 2024-10-24T14:15:00 + Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); + ControllerEssBalancingImpl.clock = clock; + + this.underTest.handleEvent(event); + + Assert.assertNull(this.underTest.currentRequest); + Assert.assertNull(this.underTest.nextRequest); + } + + @Test + public void testHandleEvent_after() { + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, new HashMap<>()); + + this.underTest.ess = new DummyManagedSymmetricEss("ess0") + .withActivePower(-100); + this.underTest.cycle = new DummyCycle(1000); + LevlControlRequest currentRequest = new LevlControlRequest(); + currentRequest.efficiencyPercent = 80; + this.underTest.currentRequest = currentRequest; + this.underTest.pucBatteryPower = 10; + this.underTest.realizedEnergyGridWs = 20; + this.underTest.realizedEnergyBatteryWs = 30; + this.underTest.levlSocWs = 40; + + this.underTest.handleEvent(event); + + //TODO: check efficiency + Assert.assertEquals(-90, this.underTest.realizedEnergyGridWs); + Assert.assertEquals(-58, this.underTest.realizedEnergyBatteryWs); + Assert.assertEquals(128, this.underTest.levlSocWs); + } + +} diff --git a/io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/MyConfig.java b/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/MyConfig.java similarity index 91% rename from io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/MyConfig.java rename to io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/MyConfig.java index cea80fb35fc..575d315f11e 100644 --- a/io.openems.edge.levl.controller/test/io/openems/edge/controller/ess/balancing/MyConfig.java +++ b/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/MyConfig.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.ess.balancing; +package io.openems.edge.levl.controller; import io.openems.common.test.AbstractComponentConfig; import io.openems.common.utils.ConfigUtils; @@ -67,11 +67,6 @@ public String meter_id() { return this.builder.meterId; } - @Override - public int targetGridSetpoint() { - return this.builder.targetGridSetpoint; - } - @Override public String ess_target() { return ConfigUtils.generateReferenceTargetFilter(this.id(), this.ess_id()); From 3b7122ce4eb3f108ca5465fe0e811740cb90feeb Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Fri, 25 Oct 2024 09:05:27 +0200 Subject: [PATCH 05/41] levl-1290/1296: add test for handleRequest and clean up LevlControlRequest --- .../ControllerEssBalancingImpl.java | 26 +++- .../levl/controller/LevlControlRequest.java | 141 ++++++++---------- .../edge/levl/controller/RequestHandler.java | 2 +- .../ControllerEssBalancingImplTest.java | 39 ++++- 4 files changed, 123 insertions(+), 85 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 9f08c701140..9d72d7fa97c 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -22,11 +22,14 @@ import io.openems.common.exceptions.InvalidValueException; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcResponse; import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.cycle.Cycle; import io.openems.edge.common.event.EdgeEventConstants; +import io.openems.edge.common.jsonapi.Call; import io.openems.edge.common.jsonapi.ComponentJsonApi; import io.openems.edge.common.jsonapi.JsonApiBuilder; import io.openems.edge.controller.api.Controller; @@ -281,14 +284,23 @@ public long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower) @Override public void buildJsonApiRoutes(JsonApiBuilder builder) { builder.handleRequest(METHOD, call -> { - var levlControlRequest = LevlControlRequest.from(call.getRequest()); - this.nextRequest = levlControlRequest; - this.levlSocWs = levlControlRequest.levlSocWh * 3600 - this.realizedEnergyBatteryWs; - return JsonrpcResponseSuccess - .from(this.generateResponse(call.getRequest().getId(), levlControlRequest.getLevlRequestId())); + return handleRequest(call); }); } + /** + * @param call + * @return + * @throws OpenemsNamedException + */ + protected JsonrpcResponse handleRequest(Call call) throws OpenemsNamedException { + var levlControlRequest = LevlControlRequest.from(call.getRequest()); + this.nextRequest = levlControlRequest; + this.levlSocWs = levlControlRequest.levlSocWh * 3600 - this.realizedEnergyBatteryWs; + return JsonrpcResponseSuccess + .from(this.generateResponse(call.getRequest().getId(), levlControlRequest.levlRequestId)); + } + private JsonObject generateResponse(UUID requestId, String levlRequestId) { JsonObject response = new JsonObject(); var result = new JsonObject(); @@ -300,7 +312,7 @@ private JsonObject generateResponse(UUID requestId, String levlRequestId) { private static boolean isActive(LevlControlRequest request) { LocalDateTime now = LocalDateTime.now(clock); - return !(request == null || now.isBefore(request.getStart()) || now.isAfter(request.getDeadline())); + return !(request == null || now.isBefore(request.start) || now.isAfter(request.deadline)); } @Override @@ -334,7 +346,7 @@ public void handleEvent(Event event) { private void finishRequest() { // Channel realizedEnergy und requestTimestamp schreiben this._setRealizedPowerW(this.realizedEnergyGridWs); - this._setLastControlRequestTimestamp(this.currentRequest.getTimestamp()); + this._setLastControlRequestTimestamp(this.currentRequest.timestamp); this.realizedEnergyGridWs = 0; this.realizedEnergyBatteryWs = 0; this.currentRequest = null; diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index 8986d135695..6a841ba161b 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -3,57 +3,55 @@ import com.google.gson.JsonObject; import io.openems.common.exceptions.OpenemsError; import io.openems.common.jsonrpc.base.JsonrpcRequest; -import io.openems.edge.levl.controller.common.Limit; import java.time.Clock; -import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.Objects; -public class LevlControlRequest extends JsonrpcRequest { - protected Clock clock = Clock.systemDefaultZone(); - +public class LevlControlRequest { public static final String METHOD = "sendLevlControlRequest"; - public int sellToGridLimitW; - public int buyFromGridLimitW; - private String levlRequestId; - private String timestamp; - public long energyWs; - public LocalDateTime start; - public LocalDateTime deadline; - public int levlSocWh; - public int socLowerBoundPercent; - public int socUpperBoundPercent; - public double efficiencyPercent; - public boolean influenceSellToGrid; - private final JsonObject params; + protected static Clock clock = Clock.systemDefaultZone(); + protected int sellToGridLimitW; + protected int buyFromGridLimitW; + protected String levlRequestId; + protected String timestamp; + protected long energyWs; + protected LocalDateTime start; + protected LocalDateTime deadline; + protected int levlSocWh; + protected int socLowerBoundPercent; + protected int socUpperBoundPercent; + protected double efficiencyPercent; + protected boolean influenceSellToGrid; + public LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String levlRequestId, + String timestamp, long energyWs, LocalDateTime start, LocalDateTime deadline, + int levlSocWh, int socLowerBoundPercent, int socUpperBoundPercent, + double efficiencyPercent, boolean influenceSellToGrid) { + this.sellToGridLimitW = sellToGridLimitW; + this.buyFromGridLimitW = buyFromGridLimitW; + this.levlRequestId = levlRequestId; + this.timestamp = timestamp; + this.energyWs = energyWs; + this.start = start; + this.deadline = deadline; + this.levlSocWh = levlSocWh; + this.socLowerBoundPercent = socLowerBoundPercent; + this.socUpperBoundPercent = socUpperBoundPercent; + this.efficiencyPercent = efficiencyPercent; + this.influenceSellToGrid = influenceSellToGrid; + } - /** - * Creates a new LevlControlRequest from a JsonrpcRequest. - * - * @param request the JsonrpcRequest - * @return a new LevlControlRequest - * @throws OpenemsError.OpenemsNamedException if the request is invalid - */ + public LevlControlRequest() { + + } + public static LevlControlRequest from(JsonrpcRequest request) throws OpenemsError.OpenemsNamedException { var params = request.getParams(); return new LevlControlRequest(params); } - public LevlControlRequest() { - super(LevlControlRequest.METHOD); - this.params = new JsonObject(); - } - - /** - * Creates a new LevlControlRequest from a JsonObject. - * - * @param params the JsonObject - * @throws OpenemsError.OpenemsNamedException if the request is invalid - */ public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedException { - super(LevlControlRequest.METHOD); - this.params = params; try { this.parseFields(params); } catch (NullPointerException e) { @@ -64,16 +62,11 @@ public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedExc if (this.efficiencyPercent < 0) { throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be > 0"); } - if (this.efficiencyPercent > 0) { + if (this.efficiencyPercent > 100) { throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be <= 100"); } } - /** - * Parses the fields from a JsonObject. - * - * @param params the JsonObject - */ private void parseFields(JsonObject params) { this.levlRequestId = params.get("levlRequestId").getAsString(); this.timestamp = params.get("levlRequestTimestamp").getAsString(); @@ -86,38 +79,34 @@ private void parseFields(JsonObject params) { this.sellToGridLimitW = params.get("sellToGridLimitW").getAsInt(); this.buyFromGridLimitW = params.get("buyFromGridLimitW").getAsInt(); this.efficiencyPercent = params.get("efficiencyPercent").getAsDouble(); - this.influenceSellToGrid = params.has("influenceSellToGrid") - ? params.get("influenceSellToGrid").getAsBoolean() + this.influenceSellToGrid = params.has("influenceSellToGrid") + ? params.get("influenceSellToGrid").getAsBoolean() : true; } - /** - * Creates a new Limit instance. - * - * @return a new Limit instance - */ - public Limit createGridPowerLimitW() { - return new Limit(this.sellToGridLimitW, this.buyFromGridLimitW); - } - - public String getLevlRequestId() { - return this.levlRequestId; - } - - public String getTimestamp() { - return this.timestamp; - } - - protected LocalDateTime getStart() { - return this.start; - } - - protected LocalDateTime getDeadline() { - return this.deadline; - } - - @Override - public JsonObject getParams() { - return this.params; - } -} \ No newline at end of file + @Override + public int hashCode() { + return Objects.hash(buyFromGridLimitW, deadline, efficiencyPercent, energyWs, influenceSellToGrid, + levlRequestId, levlSocWh, sellToGridLimitW, socLowerBoundPercent, socUpperBoundPercent, start, + timestamp); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + LevlControlRequest other = (LevlControlRequest) obj; + return buyFromGridLimitW == other.buyFromGridLimitW + && Objects.equals(deadline, other.deadline) + && Double.doubleToLongBits(efficiencyPercent) == Double.doubleToLongBits(other.efficiencyPercent) + && energyWs == other.energyWs && influenceSellToGrid == other.influenceSellToGrid + && Objects.equals(levlRequestId, other.levlRequestId) && levlSocWh == other.levlSocWh + && sellToGridLimitW == other.sellToGridLimitW && socLowerBoundPercent == other.socLowerBoundPercent + && socUpperBoundPercent == other.socUpperBoundPercent && Objects.equals(start, other.start) + && Objects.equals(timestamp, other.timestamp); + } +} diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java index b31517bbaa4..4f60b238798 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java @@ -23,7 +23,7 @@ public JsonrpcResponse addRequest(JsonrpcRequest request) throws OpenemsNamedExc var levlControlRequest = LevlControlRequest.from(request); this.requests.add(levlControlRequest); return JsonrpcResponseSuccess - .from(this.generateResponse(request.getId(), levlControlRequest.getLevlRequestId())); + .from(this.generateResponse(request.getId(), levlControlRequest.levlRequestId)); } private JsonObject generateResponse(UUID requestId, String levlRequestId) { 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 index c2efd7857b6..26c4d0feb68 100644 --- 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 @@ -11,9 +11,18 @@ import org.junit.Test; import org.osgi.service.event.Event; +import com.google.gson.JsonObject; + import io.openems.common.event.EventBuilderTest; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.AbstractJsonrpcRequestTest; +import io.openems.common.jsonrpc.base.GenericJsonrpcRequest; +import io.openems.common.jsonrpc.base.GenericJsonrpcResponseSuccess; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcResponse; +import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.edge.common.event.EdgeEventConstants; +import io.openems.edge.common.jsonapi.Call; import io.openems.edge.common.test.DummyCycle; import io.openems.edge.common.test.DummyEventAdmin; import io.openems.edge.ess.test.DummyManagedSymmetricEss; @@ -316,7 +325,7 @@ public void testHandleEvent_after() { .withActivePower(-100); this.underTest.cycle = new DummyCycle(1000); LevlControlRequest currentRequest = new LevlControlRequest(); - currentRequest.efficiencyPercent = 80; + currentRequest.efficiencyPercent = 80.0; this.underTest.currentRequest = currentRequest; this.underTest.pucBatteryPower = 10; this.underTest.realizedEnergyGridWs = 20; @@ -331,4 +340,32 @@ public void testHandleEvent_after() { Assert.assertEquals(128, this.underTest.levlSocWs); } + @Test + public void testHandleRequest() throws OpenemsNamedException { + JsonObject params = new JsonObject(); + params.addProperty("levlRequestId", "id"); + params.addProperty("levlRequestTimestamp", "2024-10-24T14:15:00Z"); + params.addProperty("levlEnergyWs", 50000L); + params.addProperty("levlChargeDelaySec", 900); + params.addProperty("levlChargeDurationSec", 900); + 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 call = new Call(request); + + // 2024-10-24T14:00:00 + Clock clock = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); + LevlControlRequest.clock = clock; + LevlControlRequest expectedNextRequest = new LevlControlRequest(3000, 4000, "id", "2024-10-24T14:15:00Z", 50000L, LocalDateTime.of(2024, 10, 24, 14, 15, 0), LocalDateTime.of(2024, 10, 24, 14, 30, 0), 10000, 20, 80, 90, true); + + this.underTest.handleRequest(call); + + Assert.assertEquals(expectedNextRequest, this.underTest.nextRequest); + Assert.assertEquals(36000000, this.underTest.levlSocWs); + } } From 3eb88cdcb8dfd49191bcc90726911bae35c57c21 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Fri, 25 Oct 2024 11:19:00 +0200 Subject: [PATCH 06/41] levl-1290/1299: add logging --- .../controller/ControllerEssBalancingImpl.java | 15 ++++++++++----- .../edge/levl/controller/LevlControlRequest.java | 9 +++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 9d72d7fa97c..73544c8c403 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -294,11 +294,13 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) { * @throws OpenemsNamedException */ protected JsonrpcResponse handleRequest(Call call) throws OpenemsNamedException { - var levlControlRequest = LevlControlRequest.from(call.getRequest()); - this.nextRequest = levlControlRequest; - this.levlSocWs = levlControlRequest.levlSocWh * 3600 - this.realizedEnergyBatteryWs; + var request = LevlControlRequest.from(call.getRequest()); + this.log.info("Received new levl request: {}", request); + this.nextRequest = request; + this.levlSocWs = request.levlSocWh * 3600 - this.realizedEnergyBatteryWs; + this.log.info("Updated levl soc: {}", this.levlSocWs); return JsonrpcResponseSuccess - .from(this.generateResponse(call.getRequest().getId(), levlControlRequest.levlRequestId)); + .from(this.generateResponse(call.getRequest().getId(), request.levlRequestId)); } private JsonObject generateResponse(UUID requestId, String levlRequestId) { @@ -324,6 +326,7 @@ public void handleEvent(Event event) { this.finishRequest(); } this.currentRequest = this.nextRequest; + this.log.info("starting levl request: {}", this.currentRequest); this.nextRequest = null; } else if (currentRequest != null && !isActive(this.currentRequest)) { this.finishRequest(); @@ -344,9 +347,11 @@ public void handleEvent(Event event) { } private void finishRequest() { - // Channel realizedEnergy und requestTimestamp schreiben + this.log.info("finished levl request: {}", this.currentRequest); this._setRealizedPowerW(this.realizedEnergyGridWs); this._setLastControlRequestTimestamp(this.currentRequest.timestamp); + this.log.info("realized levl energy on grid: {}", this.realizedEnergyGridWs); + this.log.info("realized levl energy in battery: {}", this.realizedEnergyBatteryWs); this.realizedEnergyGridWs = 0; this.realizedEnergyBatteryWs = 0; this.currentRequest = null; diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index 6a841ba161b..8d38553f803 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -109,4 +109,13 @@ public boolean equals(Object obj) { && socUpperBoundPercent == other.socUpperBoundPercent && Objects.equals(start, other.start) && Objects.equals(timestamp, other.timestamp); } + + @Override + public String toString() { + return "LevlControlRequest [sellToGridLimitW=" + sellToGridLimitW + ", buyFromGridLimitW=" + buyFromGridLimitW + + ", levlRequestId=" + levlRequestId + ", timestamp=" + timestamp + ", energyWs=" + energyWs + + ", start=" + start + ", deadline=" + deadline + ", levlSocWh=" + levlSocWh + ", socLowerBoundPercent=" + + socLowerBoundPercent + ", socUpperBoundPercent=" + socUpperBoundPercent + ", efficiencyPercent=" + + efficiencyPercent + ", influenceSellToGrid=" + influenceSellToGrid + "]"; + } } From d3f55d46005da73f7d9e89de042533536198e08a Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Fri, 25 Oct 2024 12:12:59 +0200 Subject: [PATCH 07/41] levl-1290/1295: add efficiency class --- .../levl/controller/common/Efficiency.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java new file mode 100644 index 00000000000..1c801479c35 --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java @@ -0,0 +1,39 @@ +package io.openems.edge.levl.controller.common; + +public class Efficiency { + + /** + * Applies an efficiency to a power/energy outside of the battery. + * 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 Math.round(value * efficiencyPercent/100); + } + + // discharge + return Math.round(value / (efficiencyPercent/100)); + } + + /** + * 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 applied + * @param efficiencyPercent efficiency which should be applied + * @return the power/energy outside the battery after unapplying the efficiency + */ + public static long unapply(long value, double efficiencyPercent) { + if(value <= 0) { // charge + return Math.round(value / (efficiencyPercent/100)); + } + + // discharge + return Math.round(value * efficiencyPercent/100); + } + +} \ No newline at end of file From 9a59568032ecbc08348f2479bc47e348bc936dfd Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Fri, 25 Oct 2024 14:51:26 +0200 Subject: [PATCH 08/41] levl-1290/1295: Add controller to EdgeApp.bndrun config --- io.openems.edge.application/EdgeApp.bndrun | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index 80660fc9883..1857921a1d2 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -192,6 +192,7 @@ bnd.identity;id='io.openems.edge.timeofusetariff.rabotcharge',\ bnd.identity;id='io.openems.edge.timeofusetariff.swisspower',\ bnd.identity;id='io.openems.edge.timeofusetariff.tibber',\ + bnd.identity;id='io.openems.edge.levl.controller',\ -runbundles: \ Java-WebSocket;version='[1.5.4,1.5.5)',\ @@ -433,4 +434,6 @@ org.owasp.encoder;version='[1.3.1,1.3.2)',\ reactive-streams;version='[1.0.4,1.0.5)',\ rrd4j;version='[3.9.0,3.9.1)',\ - stax2-api;version='[4.2.2,4.2.3)' \ No newline at end of file + stax2-api;version='[4.2.2,4.2.3)',\ + io.openems.edge.levl.controller;version=snapshot + \ No newline at end of file From bfbc870343b6ed1cbc6b75262dd9c2a177657081 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Tue, 29 Oct 2024 15:22:53 +0100 Subject: [PATCH 09/41] levl-1290/1298: test for null values; set request values as class variables; fix efficiency calculation --- .../ControllerEssBalancingImpl.java | 80 ++++++++++++------- .../levl/controller/LevlControlRequest.java | 5 +- .../ControllerEssBalancingImplTest.java | 9 +-- 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 73544c8c403..6c8b6707dae 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -40,12 +40,12 @@ import io.openems.edge.levl.controller.common.Efficiency; @Designate(ocd = Config.class, factory = true) -@Component(name = "Levl.Controller.Symmetric.Balancing", immediate = true, configurationPolicy = ConfigurationPolicy.REQUIRE) +@Component(name = "Controller.Levl.Symmetric.Balancing", immediate = true, configurationPolicy = ConfigurationPolicy.REQUIRE) @EventTopics({ EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, }) public class ControllerEssBalancingImpl extends AbstractOpenemsComponent - implements ControllerEssBalancing, ComponentJsonApi, EventHandler { + implements Controller, OpenemsComponent, ControllerEssBalancing, ComponentJsonApi, EventHandler { private final Logger log = LoggerFactory.getLogger(ControllerEssBalancingImpl.class); @@ -73,6 +73,13 @@ public class ControllerEssBalancingImpl extends AbstractOpenemsComponent protected int pucBatteryPower; protected long realizedEnergyGridWs; protected long realizedEnergyBatteryWs; + protected double efficiencyPercent = 100.0; + protected double socLowerBoundPercent = 0.0; + protected double socUpperBoundPercent = 100.0; + protected long energyWs; + protected int buyFromGridLimitW; + protected int sellToGridLimitW; + protected boolean influenceSellToGrid = true; private static final String METHOD = "sendLevlControlRequest"; @@ -168,8 +175,11 @@ protected int calculateRequiredPower() throws OpenemsNamedException { long nextPucSocWs = pucSocWs + Math.round(this.pucBatteryPower * cycleTimeS); // levl calculation - int levlPowerW = this.calculateLevlPowerW(this.pucBatteryPower, minEssPower, maxEssPower, pucGridPower, - nextPucSocWs, essCapacityWs, cycleTimeS); + int levlPowerW = 0; + if (this.currentRequest != null) { + levlPowerW = this.calculateLevlPowerW(this.pucBatteryPower, minEssPower, maxEssPower, pucGridPower, + nextPucSocWs, essCapacityWs, cycleTimeS); + } // overall calculation long batteryPowerW = this.pucBatteryPower + levlPowerW; @@ -214,18 +224,18 @@ protected int calculatePucBatteryPower(double cycleTimeS, int gridPower, int ess protected int applyPucSocBounds(double cycleTimeS, long essCapacityWs, long pucSocWs, int pucPower) { long dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; long dischargeEnergyUpperBoundWs = pucSocWs; - - long powerLowerBound = Efficiency.apply(Math.round(dischargeEnergyLowerBoundWs / cycleTimeS), - this.currentRequest.efficiencyPercent); - long powerUpperBound = Efficiency.apply(Math.round(dischargeEnergyUpperBoundWs / cycleTimeS), - this.currentRequest.efficiencyPercent); + + long powerLowerBound = Efficiency.unapply(Math.round(dischargeEnergyLowerBoundWs / cycleTimeS), + this.efficiencyPercent); + long powerUpperBound = Efficiency.unapply(Math.round(dischargeEnergyUpperBoundWs / cycleTimeS), + this.efficiencyPercent); return (int) Math.max(Math.min(pucPower, powerUpperBound), powerLowerBound); } private int calculateLevlPowerW(int pucBatteryPower, int minEssPower, int maxEssPower, int pucGridPower, long nextPucSocWs, long essCapacityWs, double cycleTimeS) { - long levlPower = Math.round((this.currentRequest.energyWs - this.realizedEnergyGridWs) / (double) cycleTimeS); + long levlPower = Math.round((this.energyWs - this.realizedEnergyGridWs) / (double) cycleTimeS); levlPower = this.applyBatteryPowerLimitsToLevlPower(levlPower, pucBatteryPower, minEssPower, maxEssPower); levlPower = this.applySocBoundariesToLevlPower(levlPower, nextPucSocWs, essCapacityWs, cycleTimeS); @@ -244,8 +254,8 @@ protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBattery protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long essCapacityWs, double cycleTimeS) { - long levlSocLowerBoundWs = Math.round(this.currentRequest.socLowerBoundPercent / 100.0 * essCapacityWs) - nextPucSocWs; - long levlSocUpperBoundWs = Math.round(this.currentRequest.socUpperBoundPercent / 100.0 * essCapacityWs) - nextPucSocWs; + long levlSocLowerBoundWs = Math.round(this.socLowerBoundPercent / 100.0 * essCapacityWs) - nextPucSocWs; + long levlSocUpperBoundWs = Math.round(this.socUpperBoundPercent / 100.0 * essCapacityWs) - nextPucSocWs; if (levlSocLowerBoundWs > 0) levlSocLowerBoundWs = 0; if (levlSocUpperBoundWs < 0) @@ -255,21 +265,21 @@ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - this.levlSocWs); long levlPowerLowerBound = Efficiency.unapply(Math.round(levlDischargeEnergyLowerBoundWs / cycleTimeS), - this.currentRequest.efficiencyPercent); + this.efficiencyPercent); long levlPowerUpperBound = Efficiency.unapply(Math.round(levlDischargeEnergyUpperBoundWs / cycleTimeS), - this.currentRequest.efficiencyPercent); + this.efficiencyPercent); return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower) { - long levlPowerLowerBound = -(this.currentRequest.buyFromGridLimitW - pucGridPower); - long levlPowerUpperBound = -(this.currentRequest.sellToGridLimitW - pucGridPower); + long levlPowerLowerBound = -(this.buyFromGridLimitW - pucGridPower); + long levlPowerUpperBound = -(this.sellToGridLimitW - pucGridPower); return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } public long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower) { - if (!this.currentRequest.influenceSellToGrid) { + if (!this.influenceSellToGrid) { if (pucGridPower < 0) { // if primary use case sells to grid, levl isn't allowed to do anything levlPower = 0; @@ -325,23 +335,24 @@ public void handleEvent(Event event) { if (this.currentRequest != null) { this.finishRequest(); } - this.currentRequest = this.nextRequest; - this.log.info("starting levl request: {}", this.currentRequest); - this.nextRequest = null; + startNextRequest(); } else if (currentRequest != null && !isActive(this.currentRequest)) { this.finishRequest(); } } case EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE -> { - int levlPower = 0; - if (this.ess.getActivePower().isDefined()) { - levlPower = this.ess.getActivePower().get() - this.pucBatteryPower; + if (this.currentRequest != null) { + int levlPower = 0; + if (this.ess.getActivePower().isDefined()) { + levlPower = this.ess.getActivePower().get() - this.pucBatteryPower; + } + long levlDischargeEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); + this.realizedEnergyGridWs += levlDischargeEnergyWs; + this.log.info("this cycle realized levl energy on grid: {}", levlDischargeEnergyWs); + this.realizedEnergyBatteryWs += Efficiency.apply(levlDischargeEnergyWs, + this.efficiencyPercent); + this.levlSocWs -= Efficiency.apply(levlDischargeEnergyWs, this.efficiencyPercent); } - long levlDischargeEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); - this.realizedEnergyGridWs += levlDischargeEnergyWs; - this.realizedEnergyBatteryWs += Efficiency.apply(levlDischargeEnergyWs, - this.currentRequest.efficiencyPercent); - this.levlSocWs -= Efficiency.apply(levlDischargeEnergyWs, this.currentRequest.efficiencyPercent); } } } @@ -356,4 +367,17 @@ private void finishRequest() { this.realizedEnergyBatteryWs = 0; this.currentRequest = null; } + + private void startNextRequest() { + this.log.info("starting levl request: {}", this.currentRequest); + this.currentRequest = this.nextRequest; + this.nextRequest = null; + this.efficiencyPercent = this.currentRequest.efficiencyPercent; + this.socLowerBoundPercent = this.currentRequest.socLowerBoundPercent; + this.socUpperBoundPercent = this.currentRequest.socUpperBoundPercent; + this.energyWs = this.currentRequest.energyWs; + this.buyFromGridLimitW = this.currentRequest.buyFromGridLimitW; + this.sellToGridLimitW = this.currentRequest.sellToGridLimitW; + this.influenceSellToGrid = this.currentRequest.influenceSellToGrid; + } } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index 8d38553f803..2e55b29a093 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -10,6 +10,7 @@ public class LevlControlRequest { public static final String METHOD = "sendLevlControlRequest"; + public static final int QUARTER_HOUR_SECONDS = 900; protected static Clock clock = Clock.systemDefaultZone(); protected int sellToGridLimitW; protected int buyFromGridLimitW; @@ -70,8 +71,8 @@ public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedExc private void parseFields(JsonObject params) { this.levlRequestId = params.get("levlRequestId").getAsString(); this.timestamp = params.get("levlRequestTimestamp").getAsString(); - this.energyWs = params.get("levlEnergyWs").getAsLong(); - this.start = LocalDateTime.now(this.clock).plusSeconds(params.get("levlChargeDelaySec").getAsInt()); + this.energyWs = params.get("levlPowerW").getAsLong() * QUARTER_HOUR_SECONDS; + this.start = LocalDateTime.now(LevlControlRequest.clock).plusSeconds(params.get("levlChargeDelaySec").getAsInt()); this.deadline = this.start.plusSeconds(params.get("levlChargeDurationSec").getAsInt()); this.levlSocWh = params.get("levlSocWh").getAsInt(); this.socLowerBoundPercent = params.get("levlSocLowerBoundPercent").getAsInt(); 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 index 26c4d0feb68..cf22c67a414 100644 --- 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 @@ -133,9 +133,8 @@ public void testApplyPucSocBounds() { //TODO: Efficiency prüfen Assert.assertEquals("good case discharge /w efficiency", 30, this.underTest.applyPucSocBounds(1, 100, 50, 30)); Assert.assertEquals("good case charge /w efficiency", -30, this.underTest.applyPucSocBounds(1, 100, 50, -30)); - Assert.assertEquals("minimum limit applies /w efficiency", 63, this.underTest.applyPucSocBounds(1, 100, 50, 70)); - //TODO: Ergebnis prüfen. Ist doch falsch! Wenn ich 50 reinladen kann, dann kann ich mit WK doch nicht noch weniger reinladen -// Assert.assertEquals("maximum limit applies /w efficiency", 63, this.underTest.applyPucSocBounds(1, 100, 50, -70)); + Assert.assertEquals("minimum limit applies /w efficiency", 40, this.underTest.applyPucSocBounds(1, 100, 50, 70)); + Assert.assertEquals("maximum limit applies /w efficiency", -62, this.underTest.applyPucSocBounds(1, 100, 50, -70)); } @Test @@ -345,7 +344,7 @@ public void testHandleRequest() throws OpenemsNamedException { JsonObject params = new JsonObject(); params.addProperty("levlRequestId", "id"); params.addProperty("levlRequestTimestamp", "2024-10-24T14:15:00Z"); - params.addProperty("levlEnergyWs", 50000L); + params.addProperty("levlPowerW", 500); params.addProperty("levlChargeDelaySec", 900); params.addProperty("levlChargeDurationSec", 900); params.addProperty("levlSocWh", 10000); @@ -361,7 +360,7 @@ public void testHandleRequest() throws OpenemsNamedException { // 2024-10-24T14:00:00 Clock clock = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); LevlControlRequest.clock = clock; - LevlControlRequest expectedNextRequest = new LevlControlRequest(3000, 4000, "id", "2024-10-24T14:15:00Z", 50000L, LocalDateTime.of(2024, 10, 24, 14, 15, 0), LocalDateTime.of(2024, 10, 24, 14, 30, 0), 10000, 20, 80, 90, true); + LevlControlRequest expectedNextRequest = new LevlControlRequest(3000, 4000, "id", "2024-10-24T14:15:00Z", 500*900, LocalDateTime.of(2024, 10, 24, 14, 15, 0), LocalDateTime.of(2024, 10, 24, 14, 30, 0), 10000, 20, 80, 90, true); this.underTest.handleRequest(call); From f2f5d32f8056cb85af58c0fda957c66bc5cf2412 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Tue, 29 Oct 2024 15:32:49 +0100 Subject: [PATCH 10/41] levl-1290/1303: add efficiency channel --- .../controller/ControllerEssBalancing.java | 63 ++++++++++++++----- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java index 8a99c63cd49..ccfe3166d19 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -4,6 +4,7 @@ import io.openems.common.channel.Unit; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.DoubleReadChannel; import io.openems.edge.common.channel.IntegerReadChannel; import io.openems.edge.common.channel.LongReadChannel; import io.openems.edge.common.channel.StringReadChannel; @@ -13,27 +14,31 @@ public interface ControllerEssBalancing extends Controller, OpenemsComponent { - public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { REALIZED_DISCHARGE_POWER_W(Doc.of(OpenemsType.LONG) // .unit(Unit.WATT) // .persistencePriority(PersistencePriority.HIGH) .text("the cumulated amount of discharge power that was realized since the last discharge request (in W)")), // REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING) // - .persistencePriority(PersistencePriority.HIGH) - .text("the timestamp of the last levl control request")); - - private final Doc doc; - - private ChannelId(Doc doc) { - this.doc = doc; - } - - @Override - public Doc doc() { - return this.doc; - } - } - + .persistencePriority(PersistencePriority.HIGH) // + .text("the timestamp of the last levl control request")), // + EFFICIENCY(Doc.of(OpenemsType.DOUBLE) // + .unit(Unit.PERCENT) + .persistencePriority(PersistencePriority.HIGH) // + .text("efficiency of the system defined by levl")); // + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + /** * Returns the IntegerReadChannel for the realized power. * @return the IntegerReadChannel @@ -57,7 +62,7 @@ public default Value getRealizedPowerW() { public default void _setRealizedPowerW(Long value) { this.getRealizedPowerWChannel().setNextValue(value); } - + /** * Returns the StringReadChannel for the last control request timestamp. * @return the StringReadChannel @@ -82,4 +87,28 @@ public default void _setLastControlRequestTimestamp(String value) { this.getLastControlRequestTimestampChannel().setNextValue(value); } + /** + * Returns the DoubleReadChannel for the efficiency. + * @return the DoubleReadChannel + */ + public default DoubleReadChannel getEfficiencyChannel() { + return this.channel(ChannelId.EFFICIENCY); + } + + /** + * Returns the value of the efficiency. + * @return the value of the efficiency + */ + public default Value getEfficiencyValue() { + return this.getEfficiencyChannel().value(); + } + + /** + * Sets the next value of the efficiency. + * @param value the next value + */ + public default void _setEfficiency(Double value) { + this.getEfficiencyChannel().setNextValue(value); + } + } \ No newline at end of file From e2c743b0521937a8b267eaa9ead123e1fa94ce13 Mon Sep 17 00:00:00 2001 From: MarcoLevl Date: Tue, 29 Oct 2024 18:00:36 +0100 Subject: [PATCH 11/41] levl-1290/1303: WIP: change class variables to channels --- .../controller/ControllerEssBalancing.java | 332 +++++++++++++++--- .../ControllerEssBalancingImpl.java | 172 +++++---- .../levl/controller/LevlControlRequest.java | 8 +- 3 files changed, 381 insertions(+), 131 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java index ccfe3166d19..99852e785f2 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -7,6 +7,7 @@ import io.openems.edge.common.channel.DoubleReadChannel; import io.openems.edge.common.channel.IntegerReadChannel; import io.openems.edge.common.channel.LongReadChannel; +import io.openems.edge.common.channel.BooleanReadChannel; import io.openems.edge.common.channel.StringReadChannel; import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; @@ -14,18 +15,47 @@ public interface ControllerEssBalancing extends Controller, OpenemsComponent { - public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - REALIZED_DISCHARGE_POWER_W(Doc.of(OpenemsType.LONG) // - .unit(Unit.WATT) // - .persistencePriority(PersistencePriority.HIGH) - .text("the cumulated amount of discharge power that was realized since the last discharge request (in W)")), // - REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING) // - .persistencePriority(PersistencePriority.HIGH) // - .text("the timestamp of the last levl control request")), // + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + REMAINING_LEVL_ENERGY(Doc.of(OpenemsType.LONG) + .persistencePriority(PersistencePriority.HIGH) // + .text("energy to be realized [Ws])")), // + LEVL_SOC(Doc.of(OpenemsType.LONG) + .unit(Unit.WATT_HOURS) + .persistencePriority(PersistencePriority.HIGH) // + .text("levl state of charge [Wh]")), // + SELL_TO_GRID_LIMIT(Doc.of(OpenemsType.LONG) + .unit(Unit.WATT) + .persistencePriority(PersistencePriority.HIGH) // + .text("maximum power that may be sold to the grid [W]")), // + BUY_FROM_GRID_LIMIT(Doc.of(OpenemsType.LONG) + .unit(Unit.WATT) + .persistencePriority(PersistencePriority.HIGH) // + .text("maximum power that may be bought from the grid [W]")), // + SOC_LOWER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE) + .unit(Unit.PERCENT) + .persistencePriority(PersistencePriority.HIGH) // + .text("lower soc bound limit levl has to respect [%]")), // + SOC_UPPER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE) + .unit(Unit.PERCENT) + .persistencePriority(PersistencePriority.HIGH) // + .text("upper soc bound limit levl has to respect [%]")), // + INFLUENCE_SELL_TO_GRID(Doc.of(OpenemsType.BOOLEAN) + .persistencePriority(PersistencePriority.HIGH) // + .text("defines if levl is allowed to influence the sell to grid power [true/false]")), // EFFICIENCY(Doc.of(OpenemsType.DOUBLE) // .unit(Unit.PERCENT) .persistencePriority(PersistencePriority.HIGH) // - .text("efficiency of the system defined by levl")); // + .text("ess efficiency defined by levl [%]")), // + PUC_BATTERY_POWER(Doc.of(OpenemsType.LONG) + .unit(Unit.WATT) + .persistencePriority(PersistencePriority.HIGH) // + .text("power that is applied for the ess primary use case")), // + LAST_REQUEST_REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG) // + .persistencePriority(PersistencePriority.HIGH) + .text("cumulated amount of discharge energy that has been realized since the last discharge request [Ws]")), // + LAST_REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING) // + .persistencePriority(PersistencePriority.HIGH) // + .text("the timestamp of the last levl control request")); // private final Doc doc; @@ -38,56 +68,176 @@ public Doc doc() { return this.doc; } } + + /** + * Returns the LongReadChannel for the remaining levl energy. + * @return the LongReadChannel + */ + public default LongReadChannel getRemainingLevlEnergyChannel() { + return this.channel(ChannelId.REMAINING_LEVL_ENERGY); + } + + /** + * Returns the value of the remaining levl energy. + * @return the value of the remaining levl energy + */ + public default Value getRemainingLevlEnergy() { + return this.getRemainingLevlEnergyChannel().value(); + } + + /** + * Sets the next value of the remaining levl energy. + * @param value the next value + */ + public default void _setRemainingLevlEnergy(Long value) { + this.getRemainingLevlEnergyChannel().setNextValue(value); + } + + /** + * Returns the LongReadChannel for the levl state of charge. + * @return the LongReadChannel + */ + public default LongReadChannel getLevlSocChannel() { + return this.channel(ChannelId.LEVL_SOC); + } + + /** + * Returns the value of the levl state of charge. + * @return the value of the levl state of charge + */ + public default Value getLevlSoc() { + return this.getLevlSocChannel().value(); + } + + /** + * Sets the next value of the levl state of charge. + * @param value the next value + */ + public default void _setLevlSoc(Long value) { + this.getLevlSocChannel().setNextValue(value); + } - /** - * Returns the IntegerReadChannel for the realized power. - * @return the IntegerReadChannel - */ - public default LongReadChannel getRealizedPowerWChannel() { - return this.channel(ChannelId.REALIZED_DISCHARGE_POWER_W); - } + /** + * Returns the LongReadChannel for the sell to grid limit. + * @return the LongReadChannel + */ + public default LongReadChannel getSellToGridLimitChannel() { + return this.channel(ChannelId.SELL_TO_GRID_LIMIT); + } + + /** + * Returns the value of the sell to grid limit. + * @return the value of the sell to grid limit + */ + public default Value getSellToGridLimit() { + return this.getSellToGridLimitChannel().value(); + } + + /** + * Sets the next value of the sell to grid limit. + * @param value the next value + */ + public default void _setSellToGridLimit(Long value) { + this.getSellToGridLimitChannel().setNextValue(value); + } - /** - * Returns the value of the realized power. - * @return the value of the realized power - */ - public default Value getRealizedPowerW() { - return this.getRealizedPowerWChannel().value(); - } + /** + * Returns the LongReadChannel for the buy from grid limit. + * @return the LongReadChannel + */ + public default LongReadChannel getBuyFromGridLimitChannel() { + return this.channel(ChannelId.BUY_FROM_GRID_LIMIT); + } + + /** + * Returns the value of the buy from grid limit. + * @return the value of the buy from grid limit + */ + public default Value getBuyFromGridLimit() { + return this.getBuyFromGridLimitChannel().value(); + } + + /** + * Sets the next value of the buy from grid limit. + * @param value the next value + */ + public default void _setBuyFromGridLimit(Long value) { + this.getBuyFromGridLimitChannel().setNextValue(value); + } - /** - * Sets the next value of the realized power. - * @param value the next value - */ - public default void _setRealizedPowerW(Long value) { - this.getRealizedPowerWChannel().setNextValue(value); - } + /** + * Returns the DoubleReadChannel for the lower soc bound limit. + * @return the DoubleReadChannel + */ + public default DoubleReadChannel getSocLowerBoundLevlChannel() { + return this.channel(ChannelId.SOC_LOWER_BOUND_LEVL); + } + + /** + * Returns the value of the lower soc bound limit. + * @return the value of the lower soc bound limit + */ + public default Value getSocLowerBoundLevl() { + return this.getSocLowerBoundLevlChannel().value(); + } + + /** + * Sets the next value of the lower soc bound limit. + * @param value the next value + */ + public default void _setSocLowerBoundLevl(Double value) { + this.getSocLowerBoundLevlChannel().setNextValue(value); + } - /** - * Returns the StringReadChannel for the last control request timestamp. - * @return the StringReadChannel - */ - public default StringReadChannel getLastControlRequestTimestampChannel() { - return this.channel(ChannelId.REQUEST_TIMESTAMP); - } - - /** - * Returns the value of the last control request timestamp. - * @return the value of the last control request timestamp - */ - public default Value getLastControlRequestTimestamp() { - return this.getLastControlRequestTimestampChannel().value(); - } - - /** - * Sets the next value of the last control request timestamp. - * @param value the next value - */ - public default void _setLastControlRequestTimestamp(String value) { - this.getLastControlRequestTimestampChannel().setNextValue(value); - } + /** + * Returns the DoubleReadChannel for the upper soc bound limit. + * @return the DoubleReadChannel + */ + public default DoubleReadChannel getSocUpperBoundLevlChannel() { + return this.channel(ChannelId.SOC_UPPER_BOUND_LEVL); + } + + /** + * Returns the value of the upper soc bound limit. + * @return the value of the upper soc bound limit + */ + public default Value getSocUpperBoundLevl() { + return this.getSocUpperBoundLevlChannel().value(); + } + + /** + * Sets the next value of the upper soc bound limit. + * @param value the next value + */ + public default void _setSocUpperBoundLevl(Double value) { + this.getSocUpperBoundLevlChannel().setNextValue(value); + } - /** + /** + * Returns the BooleanReadChannel for the influence sell to grid. + * @return the BooleanReadChannel + */ + public default BooleanReadChannel getInfluenceSellToGridChannel() { + return this.channel(ChannelId.INFLUENCE_SELL_TO_GRID); + } + + /** + * Returns the value of the influence sell to grid. + * @return the value of the influence sell to grid + */ + public default Value getInfluenceSellToGrid() { + return this.getInfluenceSellToGridChannel().value(); + } + + /** + * Sets the next value of the influence sell to grid. + * @param value the next value + */ + public default void _setInfluenceSellToGrid(Boolean value) { + this.getInfluenceSellToGridChannel().setNextValue(value); + } + + /** * Returns the DoubleReadChannel for the efficiency. * @return the DoubleReadChannel */ @@ -99,7 +249,7 @@ public default DoubleReadChannel getEfficiencyChannel() { * Returns the value of the efficiency. * @return the value of the efficiency */ - public default Value getEfficiencyValue() { + public default Value getEfficiency() { return this.getEfficiencyChannel().value(); } @@ -110,5 +260,77 @@ public default Value getEfficiencyValue() { public default void _setEfficiency(Double value) { this.getEfficiencyChannel().setNextValue(value); } + + /** + * Returns the LongReadChannel for the PUC battery power. + * @return the LongReadChannel + */ + public default LongReadChannel getPucBatteryPowerChannel() { + return this.channel(ChannelId.PUC_BATTERY_POWER); + } + + /** + * Returns the value of the PUC battery power. + * @return the value of the PUC battery power + */ + public default Value getPucBatteryPower() { + return this.getPucBatteryPowerChannel().value(); + } + + /** + * Sets the next value of the PUC battery power. + * @param value the next value + */ + public default void _setPucBatteryPower(Long value) { + this.getPucBatteryPowerChannel().setNextValue(value); + } + + /** + * Returns the LongReadChannel for the realized energy grid. + * @return the LongReadChannel + */ + public default LongReadChannel getLastRequestRealizedEnergyGridChannel() { + return this.channel(ChannelId.LAST_REQUEST_REALIZED_ENERGY_GRID); + } + + /** + * Returns the value of the realized energy grid. + * @return the value of the realized energy grid + */ + public default Value getLastRequestRealizedEnergyGrid() { + return this.getLastRequestRealizedEnergyGridChannel().value(); + } + + /** + * Sets the next value of the realized energy grid. + * @param value the next value + */ + public default void _setLastRequestRealizedEnergyGrid(Long value) { + this.getLastRequestRealizedEnergyGridChannel().setNextValue(value); + } + + /** + * Returns the StringReadChannel for the request timestamp. + * @return the StringReadChannel + */ + public default StringReadChannel getLastRequestTimestampChannel() { + return this.channel(ChannelId.LAST_REQUEST_TIMESTAMP); + } + + /** + * Returns the value of the request timestamp. + * @return the value of the request timestamp + */ + public default Value getLastRequestTimestamp() { + return this.getLastRequestTimestampChannel().value(); + } + + /** + * Sets the next value of the request timestamp. + * @param value the next value + */ + public default void _setLastRequestTimestamp(String value) { + this.getLastRequestTimestampChannel().setNextValue(value); + } } \ No newline at end of file diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 6c8b6707dae..bf25d3b26cb 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -69,17 +69,17 @@ public class ControllerEssBalancingImpl extends AbstractOpenemsComponent protected LevlControlRequest nextRequest; //private RequestHandler requestHandler; - protected long levlSocWs; - protected int pucBatteryPower; + //protected long levlSocWs; + //protected int pucBatteryPower; protected long realizedEnergyGridWs; protected long realizedEnergyBatteryWs; - protected double efficiencyPercent = 100.0; - protected double socLowerBoundPercent = 0.0; - protected double socUpperBoundPercent = 100.0; - protected long energyWs; - protected int buyFromGridLimitW; - protected int sellToGridLimitW; - protected boolean influenceSellToGrid = true; + //protected double efficiencyPercent = 100.0; + //protected double socLowerBoundPercent = 0.0; + //protected double socUpperBoundPercent = 100.0; + //protected long energyWs; + //protected int buyFromGridLimitW; + //protected int sellToGridLimitW; + //protected boolean influenceSellToGrid = true; private static final String METHOD = "sendLevlControlRequest"; @@ -101,6 +101,20 @@ private void activate(ComponentContext context, Config config) { if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "meter", config.meter_id())) { return; } + + this.initChannelValues(); + } + + private void initChannelValues() { + this._setLevlSoc(0L); + this._setPucBatteryPower(0L); + this._setEfficiency(0.0); + this._setSocLowerBoundLevl(0.0); + this._setSocUpperBoundLevl(0.0); + this._setRemainingLevlEnergy(0L); + this._setBuyFromGridLimit(0L); + this._setSellToGridLimit(0L); + this._setInfluenceSellToGrid(false); } @Override @@ -157,6 +171,18 @@ protected int calculateRequiredPower() throws OpenemsNamedException { int essPower = this.ess.getActivePower().getOrError(); int essCapacity = this.ess.getCapacity().getOrError(); + // TODO: Was wenn die Werte nicht gesetzt sind? + long levlSocWs = this.getLevlSoc().get(); + long remainingLevlEnergyWs = this.getRemainingLevlEnergy().get(); + double efficiency = this.getEfficiency().get(); + double socLowerBoundLevlPercent = this.getSocLowerBoundLevl().get(); + double socUpperBoundLevlPercent = this.getSocUpperBoundLevl().get(); + long buyFromGridLimit = this.getBuyFromGridLimit().get(); + long sellToGridLimit = this.getSellToGridLimit().get(); + boolean influenceSellToGrid = this.getInfluenceSellToGrid().get(); + + + //TODO: wäre das nicht der richtige Wert? G // int minEssPower = this.ess.getAllowedChargePower().getOrError(); // int maxEssPower = this.ess.getAllowedDischargePower().getOrError(); @@ -168,21 +194,22 @@ protected int calculateRequiredPower() throws OpenemsNamedException { long physicalSocWs = Math.round((physicalSoc / 100.0) * essCapacityWs); // primary use case (puc) calculation - long pucSocWs = physicalSocWs - this.levlSocWs; - this.pucBatteryPower = calculatePucBatteryPower(cycleTimeS, gridPower, essPower, - essCapacityWs, pucSocWs, minEssPower, maxEssPower); - int pucGridPower = gridPower + essPower - this.pucBatteryPower; - long nextPucSocWs = pucSocWs + Math.round(this.pucBatteryPower * cycleTimeS); + long pucSocWs = physicalSocWs - levlSocWs; + int pucBatteryPower = calculatePucBatteryPower(cycleTimeS, gridPower, essPower, + essCapacityWs, pucSocWs, minEssPower, maxEssPower, efficiency); + int pucGridPower = gridPower + essPower - pucBatteryPower; + long nextPucSocWs = pucSocWs + Math.round(pucBatteryPower * cycleTimeS); // levl calculation int levlPowerW = 0; - if (this.currentRequest != null) { - levlPowerW = this.calculateLevlPowerW(this.pucBatteryPower, minEssPower, maxEssPower, pucGridPower, - nextPucSocWs, essCapacityWs, cycleTimeS); + if (remainingLevlEnergyWs != 0) { + levlPowerW = this.calculateLevlPowerW(remainingLevlEnergyWs, pucBatteryPower, minEssPower, maxEssPower, pucGridPower, buyFromGridLimit, sellToGridLimit, + nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, socUpperBoundLevlPercent, essCapacityWs, influenceSellToGrid, efficiency, cycleTimeS); } // overall calculation - long batteryPowerW = this.pucBatteryPower + levlPowerW; + this._setPucBatteryPower((long) pucBatteryPower); + long batteryPowerW = pucBatteryPower + levlPowerW; return (int) batteryPowerW; } @@ -199,7 +226,7 @@ protected int calculateRequiredPower() throws OpenemsNamedException { * @return */ protected int calculatePucBatteryPower(double cycleTimeS, int gridPower, int essPower, - long essCapacityWs, long pucSocWs, int minEssPower, int maxEssPower) { + long essCapacityWs, long pucSocWs, int minEssPower, int maxEssPower, double efficiency) { // calculate pucPower without any limits int pucBatteryPower = gridPower + essPower; @@ -207,7 +234,7 @@ protected int calculatePucBatteryPower(double cycleTimeS, int gridPower, int ess pucBatteryPower = Math.max(Math.min(pucBatteryPower, maxEssPower), minEssPower); // apply soc bounds - pucBatteryPower = applyPucSocBounds(cycleTimeS, essCapacityWs, pucSocWs, pucBatteryPower); + pucBatteryPower = applyPucSocBounds(cycleTimeS, essCapacityWs, pucSocWs, pucBatteryPower, efficiency); return pucBatteryPower; } @@ -221,26 +248,26 @@ protected int calculatePucBatteryPower(double cycleTimeS, int gridPower, int ess * @param pucPower * @return the restricted pucPower */ - protected int applyPucSocBounds(double cycleTimeS, long essCapacityWs, long pucSocWs, int pucPower) { + protected int applyPucSocBounds(double cycleTimeS, long essCapacityWs, long pucSocWs, int pucPower, double efficiency) { long dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; long dischargeEnergyUpperBoundWs = pucSocWs; long powerLowerBound = Efficiency.unapply(Math.round(dischargeEnergyLowerBoundWs / cycleTimeS), - this.efficiencyPercent); + efficiency); long powerUpperBound = Efficiency.unapply(Math.round(dischargeEnergyUpperBoundWs / cycleTimeS), - this.efficiencyPercent); + efficiency); return (int) Math.max(Math.min(pucPower, powerUpperBound), powerLowerBound); } - private int calculateLevlPowerW(int pucBatteryPower, int minEssPower, int maxEssPower, int pucGridPower, - long nextPucSocWs, long essCapacityWs, double cycleTimeS) { - long levlPower = Math.round((this.energyWs - this.realizedEnergyGridWs) / (double) cycleTimeS); + private int calculateLevlPowerW(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) { + long levlPower = Math.round(remainingLevlEnergyWs / (double) cycleTimeS); levlPower = this.applyBatteryPowerLimitsToLevlPower(levlPower, pucBatteryPower, minEssPower, maxEssPower); - levlPower = this.applySocBoundariesToLevlPower(levlPower, nextPucSocWs, essCapacityWs, cycleTimeS); - levlPower = this.applyGridPowerLimitsToLevlPower(levlPower, pucGridPower); - levlPower = this.applyInfluenceSellToGridConstraint(levlPower, pucGridPower); + levlPower = this.applySocBoundariesToLevlPower(levlPower, nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, socUpperBoundLevlPercent, essCapacityWs, efficiency, cycleTimeS); + levlPower = this.applyGridPowerLimitsToLevlPower(levlPower, pucGridPower, buyFromGridLimit, sellToGridLimit); + levlPower = this.applyInfluenceSellToGridConstraint(levlPower, pucGridPower, influenceSellToGrid); return (int) levlPower; } @@ -252,34 +279,34 @@ protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBattery return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } - protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long essCapacityWs, - double cycleTimeS) { - long levlSocLowerBoundWs = Math.round(this.socLowerBoundPercent / 100.0 * essCapacityWs) - nextPucSocWs; - long levlSocUpperBoundWs = Math.round(this.socUpperBoundPercent / 100.0 * essCapacityWs) - nextPucSocWs; + protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long levlSocWs, double socLowerBoundLevlPercent, double socUpperBoundLevlPercent, long essCapacityWs, + double efficiency, double cycleTimeS) { + long levlSocLowerBoundWs = Math.round(socLowerBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; + long levlSocUpperBoundWs = Math.round(socUpperBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; if (levlSocLowerBoundWs > 0) levlSocLowerBoundWs = 0; if (levlSocUpperBoundWs < 0) levlSocUpperBoundWs = 0; - long levlDischargeEnergyLowerBoundWs = -(levlSocUpperBoundWs - this.levlSocWs); - long levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - this.levlSocWs); + long levlDischargeEnergyLowerBoundWs = -(levlSocUpperBoundWs - levlSocWs); + long levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - levlSocWs); long levlPowerLowerBound = Efficiency.unapply(Math.round(levlDischargeEnergyLowerBoundWs / cycleTimeS), - this.efficiencyPercent); + efficiency); long levlPowerUpperBound = Efficiency.unapply(Math.round(levlDischargeEnergyUpperBoundWs / cycleTimeS), - this.efficiencyPercent); + efficiency); return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } - protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower) { - long levlPowerLowerBound = -(this.buyFromGridLimitW - pucGridPower); - long levlPowerUpperBound = -(this.sellToGridLimitW - pucGridPower); + protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, long buyFromGridLimit, long sellToGridLimit) { + long levlPowerLowerBound = -(buyFromGridLimit - pucGridPower); + long levlPowerUpperBound = -(sellToGridLimit - pucGridPower); return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } - public long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower) { - if (!this.influenceSellToGrid) { + public 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; @@ -307,8 +334,8 @@ protected JsonrpcResponse handleRequest(Call ca var request = LevlControlRequest.from(call.getRequest()); this.log.info("Received new levl request: {}", request); this.nextRequest = request; - this.levlSocWs = request.levlSocWh * 3600 - this.realizedEnergyBatteryWs; - this.log.info("Updated levl soc: {}", this.levlSocWs); + this._setLevlSoc(request.levlSocWh * 3600 - this.realizedEnergyBatteryWs); + this.log.info("Updated levl soc: {}", this.getLevlSoc().get()); return JsonrpcResponseSuccess .from(this.generateResponse(call.getRequest().getId(), request.levlRequestId)); } @@ -330,37 +357,38 @@ private static boolean isActive(LevlControlRequest request) { @Override public void handleEvent(Event event) { switch (event.getTopic()) { - case EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS -> { - if (isActive(this.nextRequest)) { - if (this.currentRequest != null) { + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS -> { + if (isActive(this.nextRequest)) { + if (this.currentRequest != null) { + this.finishRequest(); + } + startNextRequest(); + } else if (currentRequest != null && !isActive(this.currentRequest)) { this.finishRequest(); } - startNextRequest(); - } else if (currentRequest != null && !isActive(this.currentRequest)) { - this.finishRequest(); } - } - case EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE -> { - if (this.currentRequest != null) { - int levlPower = 0; - if (this.ess.getActivePower().isDefined()) { - levlPower = this.ess.getActivePower().get() - this.pucBatteryPower; + case EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE -> { + if (this.currentRequest != null) { + long levlPower = 0; + if (this.ess.getActivePower().isDefined()) { + levlPower = this.ess.getActivePower().get() - this.getPucBatteryPower().get(); + } + long levlEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); + this._setRemainingLevlEnergy(this.getRemainingLevlEnergy().get() - levlEnergyWs); + this.realizedEnergyGridWs += levlEnergyWs; + this.log.info("this cycle realized levl energy on grid: {}", levlEnergyWs); + double efficiency = this.getEfficiency().get(); + this.realizedEnergyBatteryWs += Efficiency.apply(levlEnergyWs, efficiency); + this._setLevlSoc(this.getLevlSoc().get() - Efficiency.apply(levlEnergyWs, efficiency)); } - long levlDischargeEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); - this.realizedEnergyGridWs += levlDischargeEnergyWs; - this.log.info("this cycle realized levl energy on grid: {}", levlDischargeEnergyWs); - this.realizedEnergyBatteryWs += Efficiency.apply(levlDischargeEnergyWs, - this.efficiencyPercent); - this.levlSocWs -= Efficiency.apply(levlDischargeEnergyWs, this.efficiencyPercent); } } - } } private void finishRequest() { this.log.info("finished levl request: {}", this.currentRequest); - this._setRealizedPowerW(this.realizedEnergyGridWs); - this._setLastControlRequestTimestamp(this.currentRequest.timestamp); + this._setLastRequestRealizedEnergyGrid(this.realizedEnergyGridWs); + this._setLastRequestTimestamp(this.currentRequest.timestamp); this.log.info("realized levl energy on grid: {}", this.realizedEnergyGridWs); this.log.info("realized levl energy in battery: {}", this.realizedEnergyBatteryWs); this.realizedEnergyGridWs = 0; @@ -372,12 +400,12 @@ private void startNextRequest() { this.log.info("starting levl request: {}", this.currentRequest); this.currentRequest = this.nextRequest; this.nextRequest = null; - this.efficiencyPercent = this.currentRequest.efficiencyPercent; - this.socLowerBoundPercent = this.currentRequest.socLowerBoundPercent; - this.socUpperBoundPercent = this.currentRequest.socUpperBoundPercent; - this.energyWs = this.currentRequest.energyWs; - this.buyFromGridLimitW = this.currentRequest.buyFromGridLimitW; - this.sellToGridLimitW = this.currentRequest.sellToGridLimitW; - this.influenceSellToGrid = this.currentRequest.influenceSellToGrid; + this._setEfficiency(this.currentRequest.efficiencyPercent); + this._setSocLowerBoundLevl(this.currentRequest.socLowerBoundPercent); + this._setSocUpperBoundLevl(this.currentRequest.socUpperBoundPercent); + this._setRemainingLevlEnergy(this.currentRequest.energyWs); + this._setBuyFromGridLimit((long) this.currentRequest.buyFromGridLimitW); + this._setSellToGridLimit((long) this.currentRequest.sellToGridLimitW); + this._setInfluenceSellToGrid(this.currentRequest.influenceSellToGrid); } } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index 2e55b29a093..70ea44ac370 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -20,8 +20,8 @@ public class LevlControlRequest { protected LocalDateTime start; protected LocalDateTime deadline; protected int levlSocWh; - protected int socLowerBoundPercent; - protected int socUpperBoundPercent; + protected double socLowerBoundPercent; + protected double socUpperBoundPercent; protected double efficiencyPercent; protected boolean influenceSellToGrid; @@ -75,8 +75,8 @@ private void parseFields(JsonObject params) { this.start = LocalDateTime.now(LevlControlRequest.clock).plusSeconds(params.get("levlChargeDelaySec").getAsInt()); this.deadline = this.start.plusSeconds(params.get("levlChargeDurationSec").getAsInt()); this.levlSocWh = params.get("levlSocWh").getAsInt(); - this.socLowerBoundPercent = params.get("levlSocLowerBoundPercent").getAsInt(); - this.socUpperBoundPercent = params.get("levlSocUpperBoundPercent").getAsInt(); + this.socLowerBoundPercent = params.get("levlSocLowerBoundPercent").getAsDouble(); + this.socUpperBoundPercent = params.get("levlSocUpperBoundPercent").getAsDouble(); this.sellToGridLimitW = params.get("sellToGridLimitW").getAsInt(); this.buyFromGridLimitW = params.get("buyFromGridLimitW").getAsInt(); this.efficiencyPercent = params.get("efficiencyPercent").getAsDouble(); From 4433191af4cf8b5d722494ee75cd2cf90ae1496b Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Thu, 31 Oct 2024 19:03:03 +0100 Subject: [PATCH 12/41] levl-1290/1303: WIP: adjust unit tests after using channels in controller --- .../controller/ControllerEssBalancing.java | 3 +- .../ControllerEssBalancingImplTest.java | 109 +++++++----------- 2 files changed, 43 insertions(+), 69 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java index 99852e785f2..ca864ca4327 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -5,7 +5,6 @@ import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.channel.DoubleReadChannel; -import io.openems.edge.common.channel.IntegerReadChannel; import io.openems.edge.common.channel.LongReadChannel; import io.openems.edge.common.channel.BooleanReadChannel; import io.openems.edge.common.channel.StringReadChannel; @@ -276,7 +275,7 @@ public default LongReadChannel getPucBatteryPowerChannel() { public default Value getPucBatteryPower() { return this.getPucBatteryPowerChannel().value(); } - + /** * Sets the next value of the PUC battery power. * @param value the next value 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 index cf22c67a414..dcc3acc0901 100644 --- 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 @@ -13,18 +13,13 @@ import com.google.gson.JsonObject; -import io.openems.common.event.EventBuilderTest; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.jsonrpc.base.AbstractJsonrpcRequestTest; import io.openems.common.jsonrpc.base.GenericJsonrpcRequest; -import io.openems.common.jsonrpc.base.GenericJsonrpcResponseSuccess; import io.openems.common.jsonrpc.base.JsonrpcRequest; import io.openems.common.jsonrpc.base.JsonrpcResponse; -import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.jsonapi.Call; import io.openems.edge.common.test.DummyCycle; -import io.openems.edge.common.test.DummyEventAdmin; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.ess.test.DummyPower; import io.openems.edge.meter.test.DummyElectricityMeter; @@ -97,15 +92,15 @@ public void testCalculateRequiredPower() throws OpenemsNamedException { this.underTest.meter = new DummyElectricityMeter("meter0").withActivePower(200); this.underTest.realizedEnergyGridWs = 100; - this.underTest.levlSocWs = 2000; - this.underTest.currentRequest = new LevlControlRequest(); - this.underTest.currentRequest.energyWs = 200_000; - this.underTest.currentRequest.efficiencyPercent = 100; - this.underTest.currentRequest.socLowerBoundPercent = 20; - this.underTest.currentRequest.socUpperBoundPercent = 80; - this.underTest.currentRequest.buyFromGridLimitW = 1000; - this.underTest.currentRequest.sellToGridLimitW = -1000; - this.underTest.currentRequest.influenceSellToGrid = true; + this.underTest._setLevlSoc(2000L); //TODO: Channels lassen sich innerhalb dieses Unit-Tests nicht setzen. Ggf. nicht notwendig, da Prüfung via OpenEMS-Test +// this.underTest.currentRequest = new LevlControlRequest(); +// this.underTest.currentRequest.energyWs = 200_000; + this.underTest._setEfficiency(100.0); + this.underTest._setSocLowerBoundLevl(20.0); + this.underTest._setSocUpperBoundLevl(80.0); + this.underTest._setBuyFromGridLimit(1000L); + this.underTest._setSellToGridLimit(-1000L); + this.underTest._setInfluenceSellToGrid(true); int result = this.underTest.calculateRequiredPower(); @@ -116,38 +111,31 @@ public void testCalculateRequiredPower() throws OpenemsNamedException { @Test public void testApplyPucSocBounds() { - this.underTest.currentRequest = new LevlControlRequest(); - this.underTest.currentRequest.efficiencyPercent = 100; - - Assert.assertEquals("good case discharge", 30, this.underTest.applyPucSocBounds(1, 100, 50, 30)); - Assert.assertEquals("good case charge", -30, this.underTest.applyPucSocBounds(1, 100, 50, -30)); - Assert.assertEquals("minimum limit applies", 50, this.underTest.applyPucSocBounds(1, 100, 50, 70)); - Assert.assertEquals("minimum limit applies due to cycleTime", 25, this.underTest.applyPucSocBounds(2, 100, 50, 30)); - Assert.assertEquals("maximum limit applies", -50, this.underTest.applyPucSocBounds(1, 100, 50, -70)); - Assert.assertEquals("no charging allowed because soc is 100%", 0, this.underTest.applyPucSocBounds(1, 100, 100, -20)); - Assert.assertEquals("no discharging allowed because soc is 0%", 0, this.underTest.applyPucSocBounds(1, 100, 0, 20)); - Assert.assertEquals("discharging allowed with soc 100%", 20, this.underTest.applyPucSocBounds(1, 100, 100, 20)); - Assert.assertEquals("charging allowed with soc 0%", -20, this.underTest.applyPucSocBounds(1, 100, 0, -20)); + Assert.assertEquals("good case discharge", 30, this.underTest.applyPucSocBounds(1, 100, 50, 30, 100)); + Assert.assertEquals("good case charge", -30, this.underTest.applyPucSocBounds(1, 100, 50, -30, 100)); + Assert.assertEquals("minimum limit applies", 50, this.underTest.applyPucSocBounds(1, 100, 50, 70, 100)); + Assert.assertEquals("minimum limit applies due to cycleTime", 25, this.underTest.applyPucSocBounds(2, 100, 50, 30, 100)); + Assert.assertEquals("maximum limit applies", -50, this.underTest.applyPucSocBounds(1, 100, 50, -70, 100)); + Assert.assertEquals("no charging allowed because soc is 100%", 0, this.underTest.applyPucSocBounds(1, 100, 100, -20, 100)); + Assert.assertEquals("no discharging allowed because soc is 0%", 0, this.underTest.applyPucSocBounds(1, 100, 0, 20, 100)); + Assert.assertEquals("discharging allowed with soc 100%", 20, this.underTest.applyPucSocBounds(1, 100, 100, 20, 100)); + Assert.assertEquals("charging allowed with soc 0%", -20, this.underTest.applyPucSocBounds(1, 100, 0, -20, 100)); - this.underTest.currentRequest.efficiencyPercent = 80; - //TODO: Efficiency prüfen - Assert.assertEquals("good case discharge /w efficiency", 30, this.underTest.applyPucSocBounds(1, 100, 50, 30)); - Assert.assertEquals("good case charge /w efficiency", -30, this.underTest.applyPucSocBounds(1, 100, 50, -30)); - Assert.assertEquals("minimum limit applies /w efficiency", 40, this.underTest.applyPucSocBounds(1, 100, 50, 70)); - Assert.assertEquals("maximum limit applies /w efficiency", -62, this.underTest.applyPucSocBounds(1, 100, 50, -70)); + // efficiency 80% + Assert.assertEquals("good case discharge /w efficiency", 30, this.underTest.applyPucSocBounds(1, 100, 50, 30, 80)); + Assert.assertEquals("good case charge /w efficiency", -30, this.underTest.applyPucSocBounds(1, 100, 50, -30, 80)); + Assert.assertEquals("minimum limit applies /w efficiency", 40, this.underTest.applyPucSocBounds(1, 100, 50, 70, 80)); + Assert.assertEquals("maximum limit applies /w efficiency", -62, this.underTest.applyPucSocBounds(1, 100, 50, -70, 80)); } @Test public void testCalculatePucBatteryPower() { - this.underTest.currentRequest = new LevlControlRequest(); - this.underTest.currentRequest.efficiencyPercent = 100; - Assert.assertEquals("discharge within battery limit", 70, underTest.calculatePucBatteryPower(1, 50, 20, - 1000, 500, -150, 150)); + 1000, 500, -150, 150, 100)); Assert.assertEquals("discharge outside battery limit", 150, underTest.calculatePucBatteryPower(1, 200, 20, - 1000, 500, -150, 150)); + 1000, 500, -150, 150, 100)); Assert.assertEquals("charge outside battery limit", -150, underTest.calculatePucBatteryPower(1, -200, -20, - 1000, 500, -150, 150)); + 1000, 500, -150, 150, 100)); } // Levl Power calculation @@ -161,40 +149,27 @@ public void testApplyBatteryPowerLimitsToLevlPower() { @Test public void testApplySocBoundariesToLevlPower() { - this.underTest.currentRequest = new LevlControlRequest(); - this.underTest.currentRequest.socLowerBoundPercent = 20; - this.underTest.currentRequest.socUpperBoundPercent = 80; - this.underTest.currentRequest.efficiencyPercent = 90; - - Assert.assertEquals(-22, this.underTest.applySocBoundariesToLevlPower(-100, 60, 100, 1)); - Assert.assertEquals(-10, this.underTest.applySocBoundariesToLevlPower(-10, 60, 100, 1)); - Assert.assertEquals(10, this.underTest.applySocBoundariesToLevlPower(10, 60, 100, 1)); - Assert.assertEquals(36, this.underTest.applySocBoundariesToLevlPower(100, 60, 100, 1)); + 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() { - this.underTest.currentRequest = new LevlControlRequest(); - this.underTest.currentRequest.buyFromGridLimitW = 80; - this.underTest.currentRequest.sellToGridLimitW = -70; - - Assert.assertEquals("levlPower within limits", 50, this.underTest.applyGridPowerLimitsToLevlPower(50, 0)); - Assert.assertEquals("levlPower within limits balancing grid", 100, this.underTest.applyGridPowerLimitsToLevlPower(100, 40)); - Assert.assertEquals("levlPower constraint by sellToGridLimit", 50, this.underTest.applyGridPowerLimitsToLevlPower(100, -20)); - Assert.assertEquals("levlPower constraint by buyFromGridLimit", -60, this.underTest.applyGridPowerLimitsToLevlPower(-100, 20)); + 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() { - this.underTest.currentRequest = new LevlControlRequest(); - this.underTest.currentRequest.influenceSellToGrid = true; - - Assert.assertEquals("influence allowed", 50, this.underTest.applyInfluenceSellToGridConstraint(50, 0)); + Assert.assertEquals("influence allowed", 50, this.underTest.applyInfluenceSellToGridConstraint(50, 0, true)); - this.underTest.currentRequest.influenceSellToGrid = false; - Assert.assertEquals("buy from grid is allowed", -50, this.underTest.applyInfluenceSellToGridConstraint(-50, 20)); - Assert.assertEquals("switch gridPower /w buy from grid to sell to grid not allowed", 20, this.underTest.applyInfluenceSellToGridConstraint(50, 20)); - Assert.assertEquals("do nothing because grid power sells to grid", 0, this.underTest.applyInfluenceSellToGridConstraint(-50, -20)); + 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 @@ -323,20 +298,20 @@ public void testHandleEvent_after() { this.underTest.ess = new DummyManagedSymmetricEss("ess0") .withActivePower(-100); this.underTest.cycle = new DummyCycle(1000); + this.underTest._setPucBatteryPower(10L); //TODO: Channels lassen sich innerhalb dieses Unit-Tests nicht setzen. Ggf. nicht notwendig, da Prüfung via OpenEMS-Test + this.underTest._setLevlSoc(40L); LevlControlRequest currentRequest = new LevlControlRequest(); currentRequest.efficiencyPercent = 80.0; this.underTest.currentRequest = currentRequest; - this.underTest.pucBatteryPower = 10; this.underTest.realizedEnergyGridWs = 20; this.underTest.realizedEnergyBatteryWs = 30; - this.underTest.levlSocWs = 40; this.underTest.handleEvent(event); //TODO: check efficiency Assert.assertEquals(-90, this.underTest.realizedEnergyGridWs); Assert.assertEquals(-58, this.underTest.realizedEnergyBatteryWs); - Assert.assertEquals(128, this.underTest.levlSocWs); + Assert.assertEquals(128, this.underTest.getLevlSoc().get().longValue()); } @Test @@ -365,6 +340,6 @@ public void testHandleRequest() throws OpenemsNamedException { this.underTest.handleRequest(call); Assert.assertEquals(expectedNextRequest, this.underTest.nextRequest); - Assert.assertEquals(36000000, this.underTest.levlSocWs); + Assert.assertEquals(36000000, this.underTest.getLevlSoc().get().longValue()); //TODO: Channels lassen sich innerhalb dieses Unit-Tests nicht abfragen. Kann auch NICHT durch OpenEMS-Test abgetestet werden! } } From ab68b2c8e3a6946ee12c7a020e3244e37a2d2e41 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Thu, 31 Oct 2024 19:20:20 +0100 Subject: [PATCH 13/41] levl-1290/1308: Add tests based on OpenEMS Test Framework. --- .../ControllerEssBalancingImpl.java | 24 +- .../levl/controller/LevlControlRequest.java | 6 + .../levl/controller/BalancingImplTest.java | 397 ++++++++++++++---- 3 files changed, 341 insertions(+), 86 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index bf25d3b26cb..8fd476c8be5 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -108,7 +108,7 @@ private void activate(ComponentContext context, Config config) { private void initChannelValues() { this._setLevlSoc(0L); this._setPucBatteryPower(0L); - this._setEfficiency(0.0); + this._setEfficiency(100.0); this._setSocLowerBoundLevl(0.0); this._setSocUpperBoundLevl(0.0); this._setRemainingLevlEnergy(0L); @@ -208,7 +208,7 @@ protected int calculateRequiredPower() throws OpenemsNamedException { } // overall calculation - this._setPucBatteryPower((long) pucBatteryPower); + this._setPucBatteryPower(Long.valueOf(pucBatteryPower)); long batteryPowerW = pucBatteryPower + levlPowerW; return (int) batteryPowerW; } @@ -274,8 +274,8 @@ private int calculateLevlPowerW(long remainingLevlEnergyWs, int pucBatteryPower, protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, int maxEssPower) { - int levlPowerLowerBound = minEssPower - pucBatteryPower; - int levlPowerUpperBound = maxEssPower - pucBatteryPower; + long levlPowerLowerBound = Long.valueOf(minEssPower) - pucBatteryPower; + long levlPowerUpperBound = Long.valueOf(maxEssPower) - pucBatteryPower; return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } @@ -370,11 +370,20 @@ public void handleEvent(Event event) { case EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE -> { if (this.currentRequest != null) { long levlPower = 0; - if (this.ess.getActivePower().isDefined()) { - levlPower = this.ess.getActivePower().get() - this.getPucBatteryPower().get(); + + var essNextPower = this.ess.getDebugSetActivePowerChannel().getNextValue(); + //TODO: Bisher wurde der DebugSetActivePower verwendet. In den unteren schreiben wir jedoch rein. Welcher ist der Richtige? + // In unserem Integrationstest mal beide Werte loggen. + //var essNextPower = this.ess.getSetActivePowerEqualsWithPidChannel().getNextValue(); + if (essNextPower.isDefined()) { + var essPower = essNextPower.get(); + var pucBatteryPower = this.getPucBatteryPowerChannel().getNextValue().get(); + levlPower = essPower - pucBatteryPower; } long levlEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); + // remaining for the NEXT calculation cycle this._setRemainingLevlEnergy(this.getRemainingLevlEnergy().get() - levlEnergyWs); + // realized AFTER the next cycle (next second) this.realizedEnergyGridWs += levlEnergyWs; this.log.info("this cycle realized levl energy on grid: {}", levlEnergyWs); double efficiency = this.getEfficiency().get(); @@ -396,6 +405,8 @@ private void finishRequest() { this.currentRequest = null; } + //TODO: Values will be active with the beginning of the next cycle, because we set the "nextValue". + // There is no way to set the current value directly. However, we could run this triggered by event "BEFORE_PROCESS_IMAGE" instead of "BEFORE_CONTROLLERS". private void startNextRequest() { this.log.info("starting levl request: {}", this.currentRequest); this.currentRequest = this.nextRequest; @@ -408,4 +419,5 @@ private void startNextRequest() { this._setSellToGridLimit((long) this.currentRequest.sellToGridLimitW); this._setInfluenceSellToGrid(this.currentRequest.influenceSellToGrid); } + } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index 70ea44ac370..c99d59937d4 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -46,6 +46,12 @@ public LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String le public LevlControlRequest() { } + + public LevlControlRequest(int startDelay, int duration) { + this.start = LocalDateTime.now(LevlControlRequest.clock).plusSeconds(startDelay); + this.deadline = this.start.plusSeconds(duration); + + } public static LevlControlRequest from(JsonrpcRequest request) throws OpenemsError.OpenemsNamedException { var params = request.getParams(); 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 index 2bb54f17530..d5244b5d6c8 100644 --- 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 @@ -5,6 +5,7 @@ import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; 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; @@ -15,85 +16,321 @@ 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_SET_ACTIVE_POWER_EQUALS = new ChannelAddress(ESS_ID, -// "SetActivePowerEquals"); -// -// private static final String METER_ID = "meter0"; -// private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); -// -// @Test -// public void test() throws Exception { -// new ControllerTest(new ControllerEssBalancingImpl()) // -// .addReference("cm", new DummyConfigurationAdmin()) // -// .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // -// .setPower(new DummyPower(0.3, 0.3, 0.1))) // -// .addReference("meter", new DummyElectricityMeter(METER_ID)) // -// .activate(MyConfig.create() // -// .setId(CTRL_ID) // -// .setEssId(ESS_ID) // -// .setMeterId(METER_ID) // -// .setTargetGridSetpoint(0) // -// .build()) -// .next(new TestCase() // -// .input(ESS_ACTIVE_POWER, 0) // -// .input(METER_ACTIVE_POWER, 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)); -// } + 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, "LevlSoc"); + 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 LEVL_SOC_LOWER_BOUND = new ChannelAddress(CTRL_ID, "SocLowerBoundLevl"); + private static final ChannelAddress LEVL_SOC_UPPER_BOUND = new ChannelAddress(CTRL_ID, "SocUpperBoundLevl"); + private static final ChannelAddress LEVL_INFLUENCE_SELL_TO_GRID = new ChannelAddress(CTRL_ID, "InfluenceSellToGrid"); + private static final ChannelAddress LEVL_EFFICIENCY = new ChannelAddress(CTRL_ID, "Efficiency"); + private static final ChannelAddress LEVL_PUC_BATTERY_POWER = new ChannelAddress(CTRL_ID, "PucBatteryPower"); + private static final ChannelAddress LEVL_LAST_REQUEST_REALIZED_ENERGY_GRID = new ChannelAddress(CTRL_ID, "LastRequestRealizedEnergyGrid"); + private static final ChannelAddress LEVL_LAST_REQUEST_TIMESTAMP = new ChannelAddress(CTRL_ID, "LastRequestTimestamp"); + + @Test + public void testWithoutLevlRequest() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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) + .withAllowedChargePower(500000) //TODO: Die Werte werden in Component nicht verwendet! Herausfinden, ob wir diese berücksichtigen müssen. + .withAllowedDischargePower(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 { + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) + .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) // + .input(LEVL_REMAINING_LEVL_ENERGY, 10000) + .input(LEVL_SOC, 100_000) + .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) + .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) + .input(LEVL_SOC_LOWER_BOUND, 0) + .input(LEVL_SOC_UPPER_BOUND, 100) + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) + .input(LEVL_EFFICIENCY, 80.0) + //TODO: Prüfen ob richtiger Channel. Wurde bisher genutzt, aber ggf. nicht korrekt, da "DEBUG"? + .input(DEBUG_SET_ACTIVE_POWER, 30000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 30000) + .output(LEVL_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(LEVL_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(LEVL_PUC_BATTERY_POWER, 20000L) + .output(LEVL_REMAINING_LEVL_ENERGY, 0L) + .output(LEVL_SOC, 87500L)); + } + + @Test + public void testWithLevlChargeRequest() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) + .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) // + .input(LEVL_REMAINING_LEVL_ENERGY, -10000) + .input(LEVL_SOC, 100_000) + .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) + .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) + .input(LEVL_SOC_LOWER_BOUND, 0) + .input(LEVL_SOC_UPPER_BOUND, 100) + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) + .input(LEVL_EFFICIENCY, 80.0) + .input(DEBUG_SET_ACTIVE_POWER, 10000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 10000) + .output(LEVL_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(LEVL_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(LEVL_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 { + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) + .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) // + .input(LEVL_REMAINING_LEVL_ENERGY, 2_500_000_000L) + .input(LEVL_SOC, 100_000) + .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) + .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) + .input(LEVL_SOC_LOWER_BOUND, 0) + .input(LEVL_SOC_UPPER_BOUND, 100) + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) + .input(LEVL_EFFICIENCY, 80.0) + .input(DEBUG_SET_ACTIVE_POWER, 120000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 120000) + .output(LEVL_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(LEVL_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(LEVL_PUC_BATTERY_POWER, 20000L) + .output(LEVL_REMAINING_LEVL_ENERGY, 2_499_700_000L) + .output(LEVL_SOC, -275000L)); + } + + // Physical SoC is 90%, Levl SoC is -10%, means PUC must not charge because Levl reserved the remaining 10%. + @Test + public void testWithReservedCapacity() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMeterId(METER_ID) // + .build()) + .next(new TestCase() // + .input(ESS_SOC, 90) // 1.620.000.000 Ws + .input(ESS_ACTIVE_POWER, 0) // + .input(METER_ACTIVE_POWER, -20000) // + .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) + .input(LEVL_SOC, -180_000_000) + .input(LEVL_SELL_TO_GRID_LIMIT, -800000) + .input(LEVL_BUY_FROM_GRID_LIMIT, 800000) + .input(LEVL_SOC_LOWER_BOUND, 0) + .input(LEVL_SOC_UPPER_BOUND, 100) + .input(LEVL_INFLUENCE_SELL_TO_GRID, true) + .input(LEVL_EFFICIENCY, 80.0) + .input(DEBUG_SET_ACTIVE_POWER, -500000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500000) + .output(LEVL_PUC_BATTERY_POWER, 0L) + .output(LEVL_REMAINING_LEVL_ENERGY, -9_500_000L) + .output(LEVL_SOC, -179_600_000L)) + .next(new TestCase() // + .input(ESS_SOC, 90) // 1.620.000.000 Ws, aber eigentlich 1.620.400.000 Ws => TODO: dadurch darf der PUC wieder was machen, obwohl eigentlich noch immer alles für Levl reserviert ist. + .input(ESS_ACTIVE_POWER, -500000) // + .input(METER_ACTIVE_POWER, 480000) // + .input(DEBUG_SET_ACTIVE_POWER, -500000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500000) + .output(LEVL_PUC_BATTERY_POWER, 0L) + .output(LEVL_REMAINING_LEVL_ENERGY, -9_000_000L) + .output(LEVL_SOC, -179_200_000L)) + .next(new TestCase() // + .input(ESS_ACTIVE_POWER, -500000) // + .input(METER_ACTIVE_POWER, 480000) // + .input(DEBUG_SET_ACTIVE_POWER, -500000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500000) + .output(LEVL_PUC_BATTERY_POWER, 0L) + .output(LEVL_REMAINING_LEVL_ENERGY, -8_500_000L) + .output(LEVL_SOC, -178_800_000L)); + } + + + // Erläuterung: Channels die innerhalb des Controllers sowie in einem Testcase gesetzt werden, bleiben erhalten. + // Channels die jedoch im Anschluss durch andere Controller gesetzt werden, werden nicht gesetzt. Ebenso ändert sich z.B. nicht die ESS_ACTIVE_POWER zwischen den Zyklen, da das ESS lediglich ein Mock ist. + + //TODO: AllowedChargePower vs. ess.getPower.Max/Min. Was verwenden? Kurze Analyse: GetPower.Min/Max() wird gesetzt, wenn ApparentPower des Ess gesetzt ist. Allowed Charge/DischargePower muss nochmals irgendwo seperat gesetzt werden können. + //TODO: Müssen wir noch irgendwo die Production berücksichtigen? Z.B. an der Batterie? Eigentlich nicht, müsste alles über den NAP abgedeckt sein. } From 48b3496ff24d4f163110df64f517e23921a231dd Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Thu, 31 Oct 2024 19:25:55 +0100 Subject: [PATCH 14/41] levl-1290/1203: clean up junit tests --- .../ControllerEssBalancingImplTest.java | 51 +------------------ 1 file changed, 1 insertion(+), 50 deletions(-) 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 index dcc3acc0901..39dfb0656db 100644 --- 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 @@ -26,75 +26,27 @@ public class ControllerEssBalancingImplTest { - /* -public class CalculateSocTest { - - @Test - public void testEmpty() { - var esss = List.of(); - assertNull(new CalculateSoc().add(esss).calculate()); - } - - @Test - public void testNull() { - var esss = List.of(// - new DummySymmetricEss("ess0"), // - new DummySymmetricEss("ess1")); - assertNull(new CalculateSoc().add(esss).calculate()); - } - - @Test - public void testWeightedSoc() { - var esss = List.of(// - new DummySymmetricEss("ess0").withCapacity(10_000).withSoc(40), // - new DummySymmetricEss("ess1").withCapacity(20_000).withSoc(60)); - assertEquals(53, (int) new CalculateSoc().add(esss).calculate()); - } - - @Test - public void testAverageSoc() { - var esss = List.of(// - new DummySymmetricEss("ess0").withCapacity(10_000).withSoc(40), // - new DummySymmetricEss("ess1"), // - new DummySymmetricEss("ess2").withSoc(60)); - assertEquals(50, (int) new CalculateSoc().add(esss).calculate()); - } - -} - */ - -// @InjectMocks private ControllerEssBalancingImpl underTest; -// @Mock -// private ControllerEssBalancingImpl mockCalculator; // Mock für die Methode - @Before public void setUp() { -// MockitoAnnotations.initMocks(this); this.underTest = new ControllerEssBalancingImpl(); } @Test public void testCalculateRequiredPower() throws OpenemsNamedException { - //TODO: Keine Tests da ess, meter, ... gemockt warden müsste. this.underTest.cycle = new DummyCycle(1000); this.underTest.ess = new DummyManagedSymmetricEss("ess0") - //TODO: maximale Scheinleistung! Holen wir uns damit im Code den richtigen Wert? != allowedCharge/DischargePower - //TODO: Mit diesem Setup ist min/maxPower = MAX/MIN-Int und nur buy/sellGridLimit schränken ein .setPower(new DummyPower(0.3, 0.3, 0.1)) .withActivePower(-100) .withCapacity(500) // 1.800.000 Ws .withSoc(50) // 900.000 Ws - .withAllowedChargePower(500) - .withAllowedDischargePower(500); + .withMaxApparentPower(500); this.underTest.meter = new DummyElectricityMeter("meter0").withActivePower(200); this.underTest.realizedEnergyGridWs = 100; this.underTest._setLevlSoc(2000L); //TODO: Channels lassen sich innerhalb dieses Unit-Tests nicht setzen. Ggf. nicht notwendig, da Prüfung via OpenEMS-Test -// this.underTest.currentRequest = new LevlControlRequest(); -// this.underTest.currentRequest.energyWs = 200_000; this.underTest._setEfficiency(100.0); this.underTest._setSocLowerBoundLevl(20.0); this.underTest._setSocUpperBoundLevl(80.0); @@ -308,7 +260,6 @@ public void testHandleEvent_after() { this.underTest.handleEvent(event); - //TODO: check efficiency Assert.assertEquals(-90, this.underTest.realizedEnergyGridWs); Assert.assertEquals(-58, this.underTest.realizedEnergyBatteryWs); Assert.assertEquals(128, this.underTest.getLevlSoc().get().longValue()); From 125a356838101118eb875eea0748cf84ec04d1c8 Mon Sep 17 00:00:00 2001 From: Luca Mancilik Date: Sun, 3 Nov 2024 18:11:10 +0100 Subject: [PATCH 15/41] add integration test for reserved capacity with full percent steps, remove empty comments at line endings --- .../controller/ControllerEssBalancing.java | 48 +-- .../levl/controller/BalancingImplTest.java | 344 ++++++++++-------- 2 files changed, 223 insertions(+), 169 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java index ca864ca4327..96f71d7db02 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -16,45 +16,45 @@ public interface ControllerEssBalancing extends Controller, OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { REMAINING_LEVL_ENERGY(Doc.of(OpenemsType.LONG) - .persistencePriority(PersistencePriority.HIGH) // - .text("energy to be realized [Ws])")), // + .persistencePriority(PersistencePriority.HIGH) + .text("energy to be realized [Ws])")), LEVL_SOC(Doc.of(OpenemsType.LONG) .unit(Unit.WATT_HOURS) - .persistencePriority(PersistencePriority.HIGH) // - .text("levl state of charge [Wh]")), // + .persistencePriority(PersistencePriority.HIGH) + .text("levl state of charge [Wh]")), SELL_TO_GRID_LIMIT(Doc.of(OpenemsType.LONG) .unit(Unit.WATT) - .persistencePriority(PersistencePriority.HIGH) // - .text("maximum power that may be sold to the grid [W]")), // + .persistencePriority(PersistencePriority.HIGH) + .text("maximum power that may be sold to the grid [W]")), BUY_FROM_GRID_LIMIT(Doc.of(OpenemsType.LONG) .unit(Unit.WATT) - .persistencePriority(PersistencePriority.HIGH) // - .text("maximum power that may be bought from the grid [W]")), // + .persistencePriority(PersistencePriority.HIGH) + .text("maximum power that may be bought from the grid [W]")), SOC_LOWER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE) .unit(Unit.PERCENT) - .persistencePriority(PersistencePriority.HIGH) // - .text("lower soc bound limit levl has to respect [%]")), // + .persistencePriority(PersistencePriority.HIGH) + .text("lower soc bound limit levl has to respect [%]")), SOC_UPPER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE) .unit(Unit.PERCENT) - .persistencePriority(PersistencePriority.HIGH) // - .text("upper soc bound limit levl has to respect [%]")), // + .persistencePriority(PersistencePriority.HIGH) + .text("upper soc bound limit levl has to respect [%]")), INFLUENCE_SELL_TO_GRID(Doc.of(OpenemsType.BOOLEAN) - .persistencePriority(PersistencePriority.HIGH) // - .text("defines if levl is allowed to influence the sell to grid power [true/false]")), // - EFFICIENCY(Doc.of(OpenemsType.DOUBLE) // + .persistencePriority(PersistencePriority.HIGH) + .text("defines if levl is allowed to influence the sell to grid power [true/false]")), + EFFICIENCY(Doc.of(OpenemsType.DOUBLE) .unit(Unit.PERCENT) - .persistencePriority(PersistencePriority.HIGH) // - .text("ess efficiency defined by levl [%]")), // + .persistencePriority(PersistencePriority.HIGH) + .text("ess efficiency defined by levl [%]")), PUC_BATTERY_POWER(Doc.of(OpenemsType.LONG) .unit(Unit.WATT) - .persistencePriority(PersistencePriority.HIGH) // - .text("power that is applied for the ess primary use case")), // - LAST_REQUEST_REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG) // + .persistencePriority(PersistencePriority.HIGH) + .text("power that is applied for the ess primary use case")), + LAST_REQUEST_REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG) + .persistencePriority(PersistencePriority.HIGH) + .text("cumulated amount of discharge energy that has been realized since the last discharge request [Ws]")), + LAST_REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING) .persistencePriority(PersistencePriority.HIGH) - .text("cumulated amount of discharge energy that has been realized since the last discharge request [Ws]")), // - LAST_REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING) // - .persistencePriority(PersistencePriority.HIGH) // - .text("the timestamp of the last levl control request")); // + .text("the timestamp of the last levl control request")); private final Doc doc; 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 index d5244b5d6c8..cded0445a48 100644 --- 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 @@ -34,8 +34,8 @@ public class BalancingImplTest { private static final ChannelAddress LEVL_SOC = new ChannelAddress(CTRL_ID, "LevlSoc"); 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 LEVL_SOC_LOWER_BOUND = new ChannelAddress(CTRL_ID, "SocLowerBoundLevl"); - private static final ChannelAddress LEVL_SOC_UPPER_BOUND = new ChannelAddress(CTRL_ID, "SocUpperBoundLevl"); + private static final ChannelAddress SOC_LOWER_BOUND_LEVL = new ChannelAddress(CTRL_ID, "SocLowerBoundLevl"); + private static final ChannelAddress SOC_UPPER_BOUND_LEVL = new ChannelAddress(CTRL_ID, "SocUpperBoundLevl"); private static final ChannelAddress LEVL_INFLUENCE_SELL_TO_GRID = new ChannelAddress(CTRL_ID, "InfluenceSellToGrid"); private static final ChannelAddress LEVL_EFFICIENCY = new ChannelAddress(CTRL_ID, "Efficiency"); private static final ChannelAddress LEVL_PUC_BATTERY_POWER = new ChannelAddress(CTRL_ID, "PucBatteryPower"); @@ -44,107 +44,107 @@ public class BalancingImplTest { @Test public void testWithoutLevlRequest() throws Exception { - new ControllerTest(new ControllerEssBalancingImpl()) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // - .setPower(new DummyPower(0.3, 0.3, 0.1)) // + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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) .withAllowedChargePower(500000) //TODO: Die Werte werden in Component nicht verwendet! Herausfinden, ob wir diese berücksichtigen müssen. .withAllowedDischargePower(500000)) - .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) .addReference("cycle", new DummyCycle(1000)) - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // + .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) // + .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) // + .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 { - new ControllerTest(new ControllerEssBalancingImpl()) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // - .setPower(new DummyPower(0.3, 0.3, 0.1)) // + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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("meter", new DummyElectricityMeter(METER_ID)) .addReference("cycle", new DummyCycle(1000)) .addReference("currentRequest", new LevlControlRequest(0, 100)) - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // + .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) // + .next(new TestCase() + .input(ESS_ACTIVE_POWER, 0) + .input(METER_ACTIVE_POWER, 20000) .input(LEVL_REMAINING_LEVL_ENERGY, 10000) .input(LEVL_SOC, 100_000) .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) - .input(LEVL_SOC_LOWER_BOUND, 0) - .input(LEVL_SOC_UPPER_BOUND, 100) + .input(SOC_LOWER_BOUND_LEVL, 0) + .input(SOC_UPPER_BOUND_LEVL, 100) .input(LEVL_INFLUENCE_SELL_TO_GRID, true) .input(LEVL_EFFICIENCY, 80.0) //TODO: Prüfen ob richtiger Channel. Wurde bisher genutzt, aber ggf. nicht korrekt, da "DEBUG"? @@ -153,17 +153,17 @@ public void testWithLevlDischargeRequest() throws Exception { .output(LEVL_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) // + .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(LEVL_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) // + .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(LEVL_PUC_BATTERY_POWER, 20000L) @@ -173,30 +173,30 @@ public void testWithLevlDischargeRequest() throws Exception { @Test public void testWithLevlChargeRequest() throws Exception { - new ControllerTest(new ControllerEssBalancingImpl()) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // - .setPower(new DummyPower(0.3, 0.3, 0.1)) // + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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("meter", new DummyElectricityMeter(METER_ID)) .addReference("cycle", new DummyCycle(1000)) .addReference("currentRequest", new LevlControlRequest(0, 100)) - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // + .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) // + .next(new TestCase() + .input(ESS_ACTIVE_POWER, 0) + .input(METER_ACTIVE_POWER, 20000) .input(LEVL_REMAINING_LEVL_ENERGY, -10000) .input(LEVL_SOC, 100_000) .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) - .input(LEVL_SOC_LOWER_BOUND, 0) - .input(LEVL_SOC_UPPER_BOUND, 100) + .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(DEBUG_SET_ACTIVE_POWER, 10000) @@ -204,17 +204,17 @@ public void testWithLevlChargeRequest() throws Exception { .output(LEVL_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) // + .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(LEVL_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) // + .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(LEVL_PUC_BATTERY_POWER, 20000L) @@ -225,30 +225,30 @@ public void testWithLevlChargeRequest() throws Exception { // Test with discharge request (ws) > MAX_INT. Constrained by sell to grid limit. @Test public void testWithLargeLevlDischargeRequest() throws Exception { - new ControllerTest(new ControllerEssBalancingImpl()) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // - .setPower(new DummyPower(0.3, 0.3, 0.1)) // + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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("meter", new DummyElectricityMeter(METER_ID)) .addReference("cycle", new DummyCycle(1000)) .addReference("currentRequest", new LevlControlRequest(0, 100)) - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // + .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) // + .next(new TestCase() + .input(ESS_ACTIVE_POWER, 0) + .input(METER_ACTIVE_POWER, 20000) .input(LEVL_REMAINING_LEVL_ENERGY, 2_500_000_000L) .input(LEVL_SOC, 100_000) .input(LEVL_SELL_TO_GRID_LIMIT, -100_000) .input(LEVL_BUY_FROM_GRID_LIMIT, 100_000) - .input(LEVL_SOC_LOWER_BOUND, 0) - .input(LEVL_SOC_UPPER_BOUND, 100) + .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(DEBUG_SET_ACTIVE_POWER, 120000) @@ -256,17 +256,17 @@ public void testWithLargeLevlDischargeRequest() throws Exception { .output(LEVL_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) // + .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(LEVL_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) // + .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(LEVL_PUC_BATTERY_POWER, 20000L) @@ -277,30 +277,29 @@ public void testWithLargeLevlDischargeRequest() throws Exception { // Physical SoC is 90%, Levl SoC is -10%, means PUC must not charge because Levl reserved the remaining 10%. @Test public void testWithReservedCapacity() throws Exception { - new ControllerTest(new ControllerEssBalancingImpl()) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // - .setPower(new DummyPower(0.3, 0.3, 0.1)) // + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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("meter", new DummyElectricityMeter(METER_ID)) .addReference("cycle", new DummyCycle(1000)) .addReference("currentRequest", new LevlControlRequest(0, 100)) - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMeterId(METER_ID) // + .activate(MyConfig.create() + .setId(CTRL_ID) + .setEssId(ESS_ID) + .setMeterId(METER_ID) .build()) - .next(new TestCase() // + .next(new TestCase() .input(ESS_SOC, 90) // 1.620.000.000 Ws - .input(ESS_ACTIVE_POWER, 0) // - .input(METER_ACTIVE_POWER, -20000) // + .input(METER_ACTIVE_POWER, -20000) .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) .input(LEVL_SOC, -180_000_000) .input(LEVL_SELL_TO_GRID_LIMIT, -800000) .input(LEVL_BUY_FROM_GRID_LIMIT, 800000) - .input(LEVL_SOC_LOWER_BOUND, 0) - .input(LEVL_SOC_UPPER_BOUND, 100) + .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(DEBUG_SET_ACTIVE_POWER, -500000) @@ -308,24 +307,79 @@ public void testWithReservedCapacity() throws Exception { .output(LEVL_PUC_BATTERY_POWER, 0L) .output(LEVL_REMAINING_LEVL_ENERGY, -9_500_000L) .output(LEVL_SOC, -179_600_000L)) - .next(new TestCase() // + .next(new TestCase() .input(ESS_SOC, 90) // 1.620.000.000 Ws, aber eigentlich 1.620.400.000 Ws => TODO: dadurch darf der PUC wieder was machen, obwohl eigentlich noch immer alles für Levl reserviert ist. - .input(ESS_ACTIVE_POWER, -500000) // - .input(METER_ACTIVE_POWER, 480000) // + .input(ESS_ACTIVE_POWER, -500000) + .input(METER_ACTIVE_POWER, 480000) .input(DEBUG_SET_ACTIVE_POWER, -500000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500000) .output(LEVL_PUC_BATTERY_POWER, 0L) .output(LEVL_REMAINING_LEVL_ENERGY, -9_000_000L) .output(LEVL_SOC, -179_200_000L)) - .next(new TestCase() // - .input(ESS_ACTIVE_POWER, -500000) // - .input(METER_ACTIVE_POWER, 480000) // + .next(new TestCase() + .input(ESS_ACTIVE_POWER, -500000) + .input(METER_ACTIVE_POWER, 480000) .input(DEBUG_SET_ACTIVE_POWER, -500000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500000) .output(LEVL_PUC_BATTERY_POWER, 0L) .output(LEVL_REMAINING_LEVL_ENERGY, -8_500_000L) .output(LEVL_SOC, -178_800_000L)); } + + @Test + public void testWithLevlChargeRequestAndReservedCapacity() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) + .setPower(new DummyPower(0.3, 0.3, 0.1)) + .withCapacity(100) // = 360,000 Ws + .withMaxApparentPower(4_500)) // 3.600 / 0.8 + .addReference("meter", new DummyElectricityMeter(METER_ID)) + .addReference("cycle", new DummyCycle(1000)) + .addReference("currentRequest", new LevlControlRequest(0, 100)) + .activate(MyConfig.create() + .setId(CTRL_ID) + .setEssId(ESS_ID) + .setMeterId(METER_ID) + .build()) + .next(new TestCase() + // following values will be kept for all cycles + .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) + // following values have to be updated each cycle + .input(ESS_SOC, 90) // 90% = 90 Wh = 324,000 Ws + .input(ESS_ACTIVE_POWER, 0) + .input(METER_ACTIVE_POWER, -10_000) // grid power /wo Levl + .input(LEVL_REMAINING_LEVL_ENERGY, -28_800) // = 8 Wh + .input(LEVL_SOC, -36_000) // = 10 Wh = 10% of overall capacity + .input(DEBUG_SET_ACTIVE_POWER, -4_500) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -4_500) // max power of 4,500 W should be applied + .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl + .output(LEVL_REMAINING_LEVL_ENERGY, -24_300L) // 4,500 Ws should be realized, therefore 24,300 Ws are remaining + .output(LEVL_SOC, -32_400L)) // Levl soc rises by 4,500 Ws * 80% efficiency = 3.600 Ws = 1 Wh + .next(new TestCase() + .input(ESS_SOC, 91) // 90 Wh + 1 Wh = 91 Wh = 91% + .input(ESS_ACTIVE_POWER, -4_500) + .input(METER_ACTIVE_POWER, -5_500) // grid power ceteris paribus w/ Levl + .input(DEBUG_SET_ACTIVE_POWER, -4_500) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -4_500) + .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is still completely reserved for Levl + .output(LEVL_REMAINING_LEVL_ENERGY, -19_800L) // 4,500 Ws should be realized, therefore 19,800 Ws are remaining + .output(LEVL_SOC, -28_800L)) // Levl soc rises by 4,500 Ws * 80% efficiency = 3.600 Ws = 1 Wh + .next(new TestCase() + .input(ESS_SOC, 92) // 91 Wh + 1 Wh = 92 Wh = 92% + .input(ESS_ACTIVE_POWER, -4_500) + .input(METER_ACTIVE_POWER, -5_500) + .input(DEBUG_SET_ACTIVE_POWER, -4_500) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -4_500) + .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is still completely reserved for Levl + .output(LEVL_REMAINING_LEVL_ENERGY, -15_300L) // 4,500 Ws should be realized, therefore 15,300 Ws are remaining + .output(LEVL_SOC, -25_200L)); // Levl soc rises by 4,500 Ws * 80% efficiency = 3.600 Ws = 1 Wh + } // Erläuterung: Channels die innerhalb des Controllers sowie in einem Testcase gesetzt werden, bleiben erhalten. From 64a9f9b9ff86e96022d888414cb61b16f1b36a27 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Mon, 4 Nov 2024 19:35:06 +0100 Subject: [PATCH 16/41] levl-1290/1308: add further openems integration tests and fix edge cases --- .../ControllerEssBalancingImpl.java | 25 +- .../levl/controller/BalancingImplTest.java | 500 +++++++++++++++--- 2 files changed, 444 insertions(+), 81 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 8fd476c8be5..185f338ae38 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -194,7 +194,7 @@ protected int calculateRequiredPower() throws OpenemsNamedException { long physicalSocWs = Math.round((physicalSoc / 100.0) * essCapacityWs); // primary use case (puc) calculation - long pucSocWs = physicalSocWs - levlSocWs; + long pucSocWs = calculatePucSoc(levlSocWs, physicalSocWs); int pucBatteryPower = calculatePucBatteryPower(cycleTimeS, gridPower, essPower, essCapacityWs, pucSocWs, minEssPower, maxEssPower, efficiency); int pucGridPower = gridPower + essPower - pucBatteryPower; @@ -212,6 +212,15 @@ protected int calculateRequiredPower() throws OpenemsNamedException { long batteryPowerW = pucBatteryPower + levlPowerW; return (int) batteryPowerW; } + + protected long calculatePucSoc(long levlSocWs, long physicalSocWs) { + var pucSoc = physicalSocWs - levlSocWs; + + if (pucSoc < 0) { + return 0; + } + return pucSoc; + } /** * Calculates the power of the primary use case (puc) @@ -256,6 +265,13 @@ protected int applyPucSocBounds(double cycleTimeS, long essCapacityWs, long pucS efficiency); long powerUpperBound = Efficiency.unapply(Math.round(dischargeEnergyUpperBoundWs / cycleTimeS), efficiency); + + if (powerLowerBound > 0) { + powerLowerBound = 0; + } + if (powerUpperBound < 0) { + powerUpperBound = 0; + } return (int) Math.max(Math.min(pucPower, powerUpperBound), powerLowerBound); } @@ -283,10 +299,13 @@ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, double efficiency, double cycleTimeS) { long levlSocLowerBoundWs = Math.round(socLowerBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; long levlSocUpperBoundWs = Math.round(socUpperBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; - if (levlSocLowerBoundWs > 0) + + if (levlSocLowerBoundWs > 0) { levlSocLowerBoundWs = 0; - if (levlSocUpperBoundWs < 0) + } + if (levlSocUpperBoundWs < 0) { levlSocUpperBoundWs = 0; + } long levlDischargeEnergyLowerBoundWs = -(levlSocUpperBoundWs - levlSocWs); long levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - levlSocWs); 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 index cded0445a48..1ebb5f09f9c 100644 --- 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 @@ -9,7 +9,6 @@ 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.levl.controller.ControllerEssBalancingImpl; import io.openems.edge.meter.test.DummyElectricityMeter; public class BalancingImplTest { @@ -147,7 +146,6 @@ public void testWithLevlDischargeRequest() throws Exception { .input(SOC_UPPER_BOUND_LEVL, 100) .input(LEVL_INFLUENCE_SELL_TO_GRID, true) .input(LEVL_EFFICIENCY, 80.0) - //TODO: Prüfen ob richtiger Channel. Wurde bisher genutzt, aber ggf. nicht korrekt, da "DEBUG"? .input(DEBUG_SET_ACTIVE_POWER, 30000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 30000) .output(LEVL_PUC_BATTERY_POWER, 20000L) @@ -274,9 +272,8 @@ public void testWithLargeLevlDischargeRequest() throws Exception { .output(LEVL_SOC, -275000L)); } - // Physical SoC is 90%, Levl SoC is -10%, means PUC must not charge because Levl reserved the remaining 10%. @Test - public void testWithReservedCapacity() throws Exception { + public void testWithReservedChargeCapacityLevlChargesPucMustNotCharge() throws Exception { new ControllerTest(new ControllerEssBalancingImpl()) .addReference("cm", new DummyConfigurationAdmin()) .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) @@ -292,95 +289,442 @@ public void testWithReservedCapacity() throws Exception { .setMeterId(METER_ID) .build()) .next(new TestCase() - .input(ESS_SOC, 90) // 1.620.000.000 Ws - .input(METER_ACTIVE_POWER, -20000) - .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) - .input(LEVL_SOC, -180_000_000) + // following values will be kept for all cycles .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(DEBUG_SET_ACTIVE_POWER, -500000) - .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500000) + // 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 /wo Levl --> sell to grid + .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) + .input(LEVL_SOC, -180_000_000) // 10% of total capacity + .input(DEBUG_SET_ACTIVE_POWER, -500_000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) + .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl + .output(LEVL_REMAINING_LEVL_ENERGY, -9_500_000L) // 500,000 Ws should be realized, therefore 9,500,000 Ws are remaining + .output(LEVL_SOC, -179_600_000L)) // Levl soc increases by 500,000 Ws * 80% efficiency = 400,000 Ws + .next(new TestCase() + .input(ESS_SOC, 90) // 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_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) + .output(LEVL_PUC_BATTERY_POWER, -20_000L) // since reserved capacity decreased by 400,000 Ws in the previous cycle but ess soc value remains the same, puc can still charge + .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // 480,000 Ws can be realized for Levl, therefore 9,020,00 are remaining + .output(LEVL_SOC, -179_216_000L)); // Levl soc increases by 480,000 Ws * 80% efficiency = 384,000 Ws + } + + @Test + public void testWithReservedChargeCapacityLevlDischargesPucMustNotCharge() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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)) + .activate(MyConfig.create() + .setId(CTRL_ID) + .setEssId(ESS_ID) + .setMeterId(METER_ID) + .build()) + .next(new TestCase() + // following values will be kept for all cycles + .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) + // 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 /wo Levl --> sell to grid + .input(LEVL_REMAINING_LEVL_ENERGY, 10_000_000) + .input(LEVL_SOC, -180_000_000) // 10% of total capacity + .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) + .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl + .output(LEVL_REMAINING_LEVL_ENERGY, 9_500_000L) // 500,000 Ws should be realized, therefore 9,500,000 Ws are remaining + .output(LEVL_SOC, -180_625_000L)) // Levl soc decreases by 500,000 Ws / 80% efficiency = 625,000 Ws + .next(new TestCase() + .input(ESS_SOC, 90) // 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_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) + .output(LEVL_PUC_BATTERY_POWER, 0L) // // puc should not do anything because capacity is still completely reserved for Levl + .output(LEVL_REMAINING_LEVL_ENERGY, 9_000_000L) // 500,000 Ws should be realized, therefore 9,000,000 Ws are remaining + .output(LEVL_SOC, -181_250_000L)); // Levl soc decreases by 500,000 Ws / 80% efficiency = 625,000 Ws + } + + + @Test + public void testWithReservedChargeCapacityLevlChargesPucMayCharge() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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)) + .activate(MyConfig.create() + .setId(CTRL_ID) + .setEssId(ESS_ID) + .setMeterId(METER_ID) + .build()) + .next(new TestCase() + // following values will be kept for all cycles + .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) + // 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 /wo Levl --> sell to grid + .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) + .input(LEVL_SOC, -180_000_000) // 10% of total capacity + .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(LEVL_PUC_BATTERY_POWER, -10_000L) // puc should charge 10,000 Ws since 5% capacity is available + .output(LEVL_REMAINING_LEVL_ENERGY, -9_510_000L) // 490,000 Ws should be realized, therefore 9,510,000 Ws are remaining + .output(LEVL_SOC, -179_608_000L)) // Levl soc increases by 490,000 Ws * 80% efficiency = 392,000 Ws + .next(new TestCase() + .input(ESS_SOC, 85) // 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_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(LEVL_PUC_BATTERY_POWER, -10_000L) // puc should still charge 10,000 Ws + .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // 490,000 Ws should be realized, therefore 9,020,000 Ws are remaining + .output(LEVL_SOC, -179_216_000L)); // Levl soc increases by 490,000 Ws * 80% efficiency = 392,000 Ws + } + + @Test + public void testWithReservedChargeCapacityLevlChargesPucMayDischarge() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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)) + .activate(MyConfig.create() + .setId(CTRL_ID) + .setEssId(ESS_ID) + .setMeterId(METER_ID) + .build()) + .next(new TestCase() + // following values will be kept for all cycles + .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) + // 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 /wo Levl --> buy from grid + .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) + .input(LEVL_SOC, -180_000_000) // 10% of total capacity + .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(LEVL_PUC_BATTERY_POWER, 10_000L) // puc should discharge 10,000 Ws since Levl has reserved charge not discharge energy + .output(LEVL_REMAINING_LEVL_ENERGY, -9_490_000L) // 510,000 Ws can be realized, because puc discharges 10,000 Ws + .output(LEVL_SOC, -179_592_000L)) // Levl soc increases by 510,000 Ws * 80% efficiency = 408,000 Ws + .next(new TestCase() + .input(ESS_SOC, 50) // 50% = 250,000 Wh = 900,000,000 Ws | should be 900,400,000 Ws but only full percent values can be read + .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(LEVL_PUC_BATTERY_POWER, 10_000L) // puc should still charge 10,000 Ws + .output(LEVL_REMAINING_LEVL_ENERGY, -8_980_000L) // 510,000 Ws can be realized again, therefore 8,980,000 Ws are remaining + .output(LEVL_SOC, -179_184_000L)); // Levl soc increases by 510,000 Ws * 80% efficiency = 408,000 Ws + } + + + @Test + public void testInfluenceSellToGrid_PucSellToGrid_LevlChargeForbidden() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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)) + .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(ESS_SOC, 90) + .input(ESS_ACTIVE_POWER, 0) + .input(METER_ACTIVE_POWER, -20000) + .input(LEVL_REMAINING_LEVL_ENERGY, -10000L) + .input(LEVL_SOC, -180_000_000) + .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(DEBUG_SET_ACTIVE_POWER, 0) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) + .output(LEVL_PUC_BATTERY_POWER, 0L) + .output(LEVL_REMAINING_LEVL_ENERGY, -10000L) + .output(LEVL_SOC, -180_000_000L)); + } + + @Test + public void testInfluenceSellToGrid_PucSellToGrid_LevlDischargeForbidden() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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)) + .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(ESS_SOC, 90) + .input(ESS_ACTIVE_POWER, 0) + .input(METER_ACTIVE_POWER, -20000) + .input(LEVL_REMAINING_LEVL_ENERGY, 10000L) + .input(LEVL_SOC, -180_000_000) + .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(DEBUG_SET_ACTIVE_POWER, 0) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) .output(LEVL_PUC_BATTERY_POWER, 0L) - .output(LEVL_REMAINING_LEVL_ENERGY, -9_500_000L) - .output(LEVL_SOC, -179_600_000L)) - .next(new TestCase() - .input(ESS_SOC, 90) // 1.620.000.000 Ws, aber eigentlich 1.620.400.000 Ws => TODO: dadurch darf der PUC wieder was machen, obwohl eigentlich noch immer alles für Levl reserviert ist. - .input(ESS_ACTIVE_POWER, -500000) - .input(METER_ACTIVE_POWER, 480000) - .input(DEBUG_SET_ACTIVE_POWER, -500000) - .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500000) + .output(LEVL_REMAINING_LEVL_ENERGY, 10000L) + .output(LEVL_SOC, -180_000_000L)); + } + + @Test + public void testInfluenceSellToGrid_PucBuyFromGrid_LevlChargeAllowed() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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)) + .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(ESS_SOC, 0) + .input(ESS_ACTIVE_POWER, 0) + .input(METER_ACTIVE_POWER, 20000) + .input(LEVL_REMAINING_LEVL_ENERGY, -10000L) + .input(LEVL_SOC, 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, 80.0) + .input(DEBUG_SET_ACTIVE_POWER, -10000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -10000) .output(LEVL_PUC_BATTERY_POWER, 0L) - .output(LEVL_REMAINING_LEVL_ENERGY, -9_000_000L) - .output(LEVL_SOC, -179_200_000L)) - .next(new TestCase() - .input(ESS_ACTIVE_POWER, -500000) - .input(METER_ACTIVE_POWER, 480000) - .input(DEBUG_SET_ACTIVE_POWER, -500000) - .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500000) + .output(LEVL_REMAINING_LEVL_ENERGY, 0L) + .output(LEVL_SOC, 8000L)); + } + + @Test + public void testInfluenceSellToGrid_PucBuyFromGrid_LevlDischargeLimited() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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)) + .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(ESS_SOC, 0) + .input(ESS_ACTIVE_POWER, 0) + .input(METER_ACTIVE_POWER, 20000) + .input(LEVL_REMAINING_LEVL_ENERGY, -10000L) + .input(LEVL_SOC, 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(DEBUG_SET_ACTIVE_POWER, -10000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -10000) .output(LEVL_PUC_BATTERY_POWER, 0L) - .output(LEVL_REMAINING_LEVL_ENERGY, -8_500_000L) - .output(LEVL_SOC, -178_800_000L)); + .output(LEVL_REMAINING_LEVL_ENERGY, 0L) + .output(LEVL_SOC, 10000L)); } @Test - public void testWithLevlChargeRequestAndReservedCapacity() throws Exception { + public void testUpperSocLimit() throws Exception { new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) - .withCapacity(100) // = 360,000 Ws - .withMaxApparentPower(4_500)) // 3.600 / 0.8 - .addReference("meter", new DummyElectricityMeter(METER_ID)) - .addReference("cycle", new DummyCycle(1000)) - .addReference("currentRequest", new LevlControlRequest(0, 100)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() - // following values will be kept for all cycles - .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) - // following values have to be updated each cycle - .input(ESS_SOC, 90) // 90% = 90 Wh = 324,000 Ws - .input(ESS_ACTIVE_POWER, 0) - .input(METER_ACTIVE_POWER, -10_000) // grid power /wo Levl - .input(LEVL_REMAINING_LEVL_ENERGY, -28_800) // = 8 Wh - .input(LEVL_SOC, -36_000) // = 10 Wh = 10% of overall capacity - .input(DEBUG_SET_ACTIVE_POWER, -4_500) - .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -4_500) // max power of 4,500 W should be applied - .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl - .output(LEVL_REMAINING_LEVL_ENERGY, -24_300L) // 4,500 Ws should be realized, therefore 24,300 Ws are remaining - .output(LEVL_SOC, -32_400L)) // Levl soc rises by 4,500 Ws * 80% efficiency = 3.600 Ws = 1 Wh - .next(new TestCase() - .input(ESS_SOC, 91) // 90 Wh + 1 Wh = 91 Wh = 91% - .input(ESS_ACTIVE_POWER, -4_500) - .input(METER_ACTIVE_POWER, -5_500) // grid power ceteris paribus w/ Levl - .input(DEBUG_SET_ACTIVE_POWER, -4_500) - .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -4_500) - .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is still completely reserved for Levl - .output(LEVL_REMAINING_LEVL_ENERGY, -19_800L) // 4,500 Ws should be realized, therefore 19,800 Ws are remaining - .output(LEVL_SOC, -28_800L)) // Levl soc rises by 4,500 Ws * 80% efficiency = 3.600 Ws = 1 Wh - .next(new TestCase() - .input(ESS_SOC, 92) // 91 Wh + 1 Wh = 92 Wh = 92% - .input(ESS_ACTIVE_POWER, -4_500) - .input(METER_ACTIVE_POWER, -5_500) - .input(DEBUG_SET_ACTIVE_POWER, -4_500) - .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -4_500) - .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is still completely reserved for Levl - .output(LEVL_REMAINING_LEVL_ENERGY, -15_300L) // 4,500 Ws should be realized, therefore 15,300 Ws are remaining - .output(LEVL_SOC, -25_200L)); // Levl soc rises by 4,500 Ws * 80% efficiency = 3.600 Ws = 1 Wh + .addReference("cm", new DummyConfigurationAdmin()) + .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)) + .activate(MyConfig.create() + .setId(CTRL_ID) + .setEssId(ESS_ID) + .setMeterId(METER_ID) + .build()) + .next(new TestCase() + .input(ESS_SOC, 79) + .input(ESS_ACTIVE_POWER, 0) + .input(METER_ACTIVE_POWER, 0) + .input(LEVL_REMAINING_LEVL_ENERGY, -100_000_000) + .input(LEVL_SOC, 0) + .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(DEBUG_SET_ACTIVE_POWER, -18_000_000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -18_000_000) + .output(LEVL_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(LEVL_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(LEVL_PUC_BATTERY_POWER, 0L) + .output(LEVL_REMAINING_LEVL_ENERGY, -82_000_000L) + .output(LEVL_SOC, 18_000_000L)); } + @Test + public void testLowerSocLimit() throws Exception { + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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)) + .activate(MyConfig.create() + .setId(CTRL_ID) + .setEssId(ESS_ID) + .setMeterId(METER_ID) + .build()) + .next(new TestCase() + .input(ESS_SOC, 21) + .input(ESS_ACTIVE_POWER, 0) + .input(METER_ACTIVE_POWER, 0) + .input(LEVL_REMAINING_LEVL_ENERGY, 100_000_000) + .input(LEVL_SOC, 0) + .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(DEBUG_SET_ACTIVE_POWER, 18_000_000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 18_000_000) + .output(LEVL_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(LEVL_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(LEVL_PUC_BATTERY_POWER, 0L) + .output(LEVL_REMAINING_LEVL_ENERGY, 82_000_000L) + .output(LEVL_SOC, -18_000_000L)); + } + + // Test cases EVO + + // x: charge request + // x: discharge request + // x: avoid integer overflow with large request + + // x: reserved charge energy, levl charges, puc should not charge + // x: reserved charge energy, levl discharges, puc should not charge + + // x: reserved charge energy, puc may charge + // x: reserved charge energy, puc may discharge + + // x: charge request, forbid influence sell to grid + // x: discharge request, forbid influence sell to grid + + // update levl soc => nicht umsetzbar, da wir keinen Request simulieren können + + // x respect grid limits => integer overflow x sell to grid limit + // respect lower soc limit + // x: respect upper soc limit + // Erläuterung: Channels die innerhalb des Controllers sowie in einem Testcase gesetzt werden, bleiben erhalten. // Channels die jedoch im Anschluss durch andere Controller gesetzt werden, werden nicht gesetzt. Ebenso ändert sich z.B. nicht die ESS_ACTIVE_POWER zwischen den Zyklen, da das ESS lediglich ein Mock ist. From 2746309f5e978417a76898380d866fcc266a2414 Mon Sep 17 00:00:00 2001 From: MarcoLevl Date: Tue, 5 Nov 2024 15:54:01 +0100 Subject: [PATCH 17/41] levl-1290/1308: review and minor changes of integration tests --- .../levl/controller/BalancingImplTest.java | 172 ++++++++---------- 1 file changed, 76 insertions(+), 96 deletions(-) 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 index 1ebb5f09f9c..7efe7c0867f 100644 --- 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 @@ -38,8 +38,6 @@ public class BalancingImplTest { private static final ChannelAddress LEVL_INFLUENCE_SELL_TO_GRID = new ChannelAddress(CTRL_ID, "InfluenceSellToGrid"); private static final ChannelAddress LEVL_EFFICIENCY = new ChannelAddress(CTRL_ID, "Efficiency"); private static final ChannelAddress LEVL_PUC_BATTERY_POWER = new ChannelAddress(CTRL_ID, "PucBatteryPower"); - private static final ChannelAddress LEVL_LAST_REQUEST_REALIZED_ENERGY_GRID = new ChannelAddress(CTRL_ID, "LastRequestRealizedEnergyGrid"); - private static final ChannelAddress LEVL_LAST_REQUEST_TIMESTAMP = new ChannelAddress(CTRL_ID, "LastRequestTimestamp"); @Test public void testWithoutLevlRequest() throws Exception { @@ -136,16 +134,18 @@ public void testWithLevlDischargeRequest() throws Exception { .setMeterId(METER_ID) .build()) .next(new TestCase() - .input(ESS_ACTIVE_POWER, 0) - .input(METER_ACTIVE_POWER, 20000) - .input(LEVL_REMAINING_LEVL_ENERGY, 10000) - .input(LEVL_SOC, 100_000) + // 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(LEVL_PUC_BATTERY_POWER, 20000L) @@ -187,16 +187,18 @@ public void testWithLevlChargeRequest() throws Exception { .setMeterId(METER_ID) .build()) .next(new TestCase() - .input(ESS_ACTIVE_POWER, 0) - .input(METER_ACTIVE_POWER, 20000) - .input(LEVL_REMAINING_LEVL_ENERGY, -10000) - .input(LEVL_SOC, 100_000) + // 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(LEVL_PUC_BATTERY_POWER, 20000L) @@ -239,16 +241,18 @@ public void testWithLargeLevlDischargeRequest() throws Exception { .setMeterId(METER_ID) .build()) .next(new TestCase() - .input(ESS_ACTIVE_POWER, 0) - .input(METER_ACTIVE_POWER, 20000) - .input(LEVL_REMAINING_LEVL_ENERGY, 2_500_000_000L) - .input(LEVL_SOC, 100_000) + // 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(LEVL_PUC_BATTERY_POWER, 20000L) @@ -289,19 +293,19 @@ public void testWithReservedChargeCapacityLevlChargesPucMustNotCharge() throws E .setMeterId(METER_ID) .build()) .next(new TestCase() - // following values will be kept for all cycles + // 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 /wo Levl --> sell to grid - .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) - .input(LEVL_SOC, -180_000_000) // 10% of total capacity + .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) .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl @@ -313,7 +317,7 @@ public void testWithReservedChargeCapacityLevlChargesPucMustNotCharge() throws E .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) - .output(LEVL_PUC_BATTERY_POWER, -20_000L) // since reserved capacity decreased by 400,000 Ws in the previous cycle but ess soc value remains the same, puc can still charge + .output(LEVL_PUC_BATTERY_POWER, -20_000L) // since reserved capacity decreased by 400,000 Ws in the previous cycle but ess soc value remains the same, puc can charge again .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // 480,000 Ws can be realized for Levl, therefore 9,020,00 are remaining .output(LEVL_SOC, -179_216_000L)); // Levl soc increases by 480,000 Ws * 80% efficiency = 384,000 Ws } @@ -335,19 +339,19 @@ public void testWithReservedChargeCapacityLevlDischargesPucMustNotCharge() throw .setMeterId(METER_ID) .build()) .next(new TestCase() - // following values will be kept for all cycles + // 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 /wo Levl --> sell to grid - .input(LEVL_REMAINING_LEVL_ENERGY, 10_000_000) - .input(LEVL_SOC, -180_000_000) // 10% of total capacity + .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) .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl @@ -382,19 +386,19 @@ public void testWithReservedChargeCapacityLevlChargesPucMayCharge() throws Excep .setMeterId(METER_ID) .build()) .next(new TestCase() - // following values will be kept for all cycles + // 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 /wo Levl --> sell to grid - .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) - .input(LEVL_SOC, -180_000_000) // 10% of total capacity + .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) .output(LEVL_PUC_BATTERY_POWER, -10_000L) // puc should charge 10,000 Ws since 5% capacity is available @@ -428,19 +432,19 @@ public void testWithReservedChargeCapacityLevlChargesPucMayDischarge() throws Ex .setMeterId(METER_ID) .build()) .next(new TestCase() - // following values will be kept for all cycles + // 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 /wo Levl --> buy from grid - .input(LEVL_REMAINING_LEVL_ENERGY, -10_000_000) - .input(LEVL_SOC, -180_000_000) // 10% of total capacity + .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) .output(LEVL_PUC_BATTERY_POWER, 10_000L) // puc should discharge 10,000 Ws since Levl has reserved charge not discharge energy @@ -475,17 +479,17 @@ public void testInfluenceSellToGrid_PucSellToGrid_LevlChargeForbidden() throws E .setMeterId(METER_ID) .build()) .next(new TestCase("puc sell to grid, levl charge not allowed") - .input(ESS_SOC, 90) - .input(ESS_ACTIVE_POWER, 0) - .input(METER_ACTIVE_POWER, -20000) - .input(LEVL_REMAINING_LEVL_ENERGY, -10000L) - .input(LEVL_SOC, -180_000_000) .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(LEVL_PUC_BATTERY_POWER, 0L) @@ -510,17 +514,17 @@ public void testInfluenceSellToGrid_PucSellToGrid_LevlDischargeForbidden() throw .setMeterId(METER_ID) .build()) .next(new TestCase("puc sell to grid, levl discharge not allowed") - .input(ESS_SOC, 90) - .input(ESS_ACTIVE_POWER, 0) - .input(METER_ACTIVE_POWER, -20000) - .input(LEVL_REMAINING_LEVL_ENERGY, 10000L) - .input(LEVL_SOC, -180_000_000) .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(LEVL_PUC_BATTERY_POWER, 0L) @@ -545,22 +549,22 @@ public void testInfluenceSellToGrid_PucBuyFromGrid_LevlChargeAllowed() throws Ex .setMeterId(METER_ID) .build()) .next(new TestCase("puc buy from grid, levl charge is allowed") - .input(ESS_SOC, 0) - .input(ESS_ACTIVE_POWER, 0) - .input(METER_ACTIVE_POWER, 20000) - .input(LEVL_REMAINING_LEVL_ENERGY, -10000L) - .input(LEVL_SOC, 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, 80.0) - .input(DEBUG_SET_ACTIVE_POWER, -10000) - .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -10000) + .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(LEVL_PUC_BATTERY_POWER, 0L) .output(LEVL_REMAINING_LEVL_ENERGY, 0L) - .output(LEVL_SOC, 8000L)); + .output(LEVL_SOC, 24000L)); } @Test @@ -580,22 +584,22 @@ public void testInfluenceSellToGrid_PucBuyFromGrid_LevlDischargeLimited() throws .setMeterId(METER_ID) .build()) .next(new TestCase("puc buy from grid, levl discharge is limited to grid limit 0") - .input(ESS_SOC, 0) - .input(ESS_ACTIVE_POWER, 0) - .input(METER_ACTIVE_POWER, 20000) - .input(LEVL_REMAINING_LEVL_ENERGY, -10000L) - .input(LEVL_SOC, 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(DEBUG_SET_ACTIVE_POWER, -10000) - .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -10000) + .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(LEVL_PUC_BATTERY_POWER, 0L) - .output(LEVL_REMAINING_LEVL_ENERGY, 0L) - .output(LEVL_SOC, 10000L)); + .output(LEVL_REMAINING_LEVL_ENERGY, 10000L) + .output(LEVL_SOC, 179_980_000L)); } @Test @@ -615,17 +619,19 @@ public void testUpperSocLimit() throws Exception { .setMeterId(METER_ID) .build()) .next(new TestCase() - .input(ESS_SOC, 79) - .input(ESS_ACTIVE_POWER, 0) - .input(METER_ACTIVE_POWER, 0) - .input(LEVL_REMAINING_LEVL_ENERGY, -100_000_000) - .input(LEVL_SOC, 0) + // 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(LEVL_PUC_BATTERY_POWER, -0L) @@ -668,17 +674,19 @@ public void testLowerSocLimit() throws Exception { .setMeterId(METER_ID) .build()) .next(new TestCase() - .input(ESS_SOC, 21) - .input(ESS_ACTIVE_POWER, 0) - .input(METER_ACTIVE_POWER, 0) - .input(LEVL_REMAINING_LEVL_ENERGY, 100_000_000) - .input(LEVL_SOC, 0) + // 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(LEVL_PUC_BATTERY_POWER, -0L) @@ -703,32 +711,4 @@ public void testLowerSocLimit() throws Exception { .output(LEVL_REMAINING_LEVL_ENERGY, 82_000_000L) .output(LEVL_SOC, -18_000_000L)); } - - // Test cases EVO - - // x: charge request - // x: discharge request - // x: avoid integer overflow with large request - - // x: reserved charge energy, levl charges, puc should not charge - // x: reserved charge energy, levl discharges, puc should not charge - - // x: reserved charge energy, puc may charge - // x: reserved charge energy, puc may discharge - - // x: charge request, forbid influence sell to grid - // x: discharge request, forbid influence sell to grid - - // update levl soc => nicht umsetzbar, da wir keinen Request simulieren können - - // x respect grid limits => integer overflow x sell to grid limit - // respect lower soc limit - // x: respect upper soc limit - - - // Erläuterung: Channels die innerhalb des Controllers sowie in einem Testcase gesetzt werden, bleiben erhalten. - // Channels die jedoch im Anschluss durch andere Controller gesetzt werden, werden nicht gesetzt. Ebenso ändert sich z.B. nicht die ESS_ACTIVE_POWER zwischen den Zyklen, da das ESS lediglich ein Mock ist. - - //TODO: AllowedChargePower vs. ess.getPower.Max/Min. Was verwenden? Kurze Analyse: GetPower.Min/Max() wird gesetzt, wenn ApparentPower des Ess gesetzt ist. Allowed Charge/DischargePower muss nochmals irgendwo seperat gesetzt werden können. - //TODO: Müssen wir noch irgendwo die Production berücksichtigen? Z.B. an der Batterie? Eigentlich nicht, müsste alles über den NAP abgedeckt sein. } From 8206db423cb944817018d5530c39ff6b9c36dcb4 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Tue, 5 Nov 2024 16:08:01 +0100 Subject: [PATCH 18/41] levl-1290/1303: Fix unit tests with channels; adjust channel error handling in impl; move request handling to before_process_image event; clean up files --- .../ControllerEssBalancingImpl.java | 120 ++++++++---------- .../ControllerEssBalancingImplTest.java | 105 ++++++++------- 2 files changed, 109 insertions(+), 116 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 185f338ae38..ea7f0864d4c 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -61,25 +61,12 @@ public class ControllerEssBalancingImpl extends AbstractOpenemsComponent @Reference protected Cycle cycle; - private Config config; - protected static Clock clock = Clock.systemDefaultZone(); protected LevlControlRequest currentRequest; protected LevlControlRequest nextRequest; - - //private RequestHandler requestHandler; - //protected long levlSocWs; - //protected int pucBatteryPower; protected long realizedEnergyGridWs; protected long realizedEnergyBatteryWs; - //protected double efficiencyPercent = 100.0; - //protected double socLowerBoundPercent = 0.0; - //protected double socUpperBoundPercent = 100.0; - //protected long energyWs; - //protected int buyFromGridLimitW; - //protected int sellToGridLimitW; - //protected boolean influenceSellToGrid = true; private static final String METHOD = "sendLevlControlRequest"; @@ -94,7 +81,6 @@ public ControllerEssBalancingImpl() { @Activate private void activate(ComponentContext context, Config config) { super.activate(context, config.id(), config.alias(), config.enabled()); - this.config = config; if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "ess", config.ess_id())) { return; } @@ -104,18 +90,6 @@ private void activate(ComponentContext context, Config config) { this.initChannelValues(); } - - private void initChannelValues() { - this._setLevlSoc(0L); - this._setPucBatteryPower(0L); - this._setEfficiency(100.0); - this._setSocLowerBoundLevl(0.0); - this._setSocUpperBoundLevl(0.0); - this._setRemainingLevlEnergy(0L); - this._setBuyFromGridLimit(0L); - this._setSellToGridLimit(0L); - this._setInfluenceSellToGrid(false); - } @Override @Deactivate @@ -151,6 +125,19 @@ public void run() throws OpenemsNamedException { this.ess.setActivePowerEqualsWithPid(calculatedPower); this.ess.setReactivePowerEquals(0); } + + + private void initChannelValues() { + this._setLevlSoc(0L); + this._setPucBatteryPower(0L); + this._setEfficiency(100.0); + this._setSocLowerBoundLevl(0.0); + this._setSocUpperBoundLevl(0.0); + this._setRemainingLevlEnergy(0L); + this._setBuyFromGridLimit(0L); + this._setSellToGridLimit(0L); + this._setInfluenceSellToGrid(false); + } /** * Calculates required charge/discharge power. @@ -171,21 +158,14 @@ protected int calculateRequiredPower() throws OpenemsNamedException { int essPower = this.ess.getActivePower().getOrError(); int essCapacity = this.ess.getCapacity().getOrError(); - // TODO: Was wenn die Werte nicht gesetzt sind? - long levlSocWs = this.getLevlSoc().get(); - long remainingLevlEnergyWs = this.getRemainingLevlEnergy().get(); - double efficiency = this.getEfficiency().get(); - double socLowerBoundLevlPercent = this.getSocLowerBoundLevl().get(); - double socUpperBoundLevlPercent = this.getSocUpperBoundLevl().get(); - long buyFromGridLimit = this.getBuyFromGridLimit().get(); - long sellToGridLimit = this.getSellToGridLimit().get(); - boolean influenceSellToGrid = this.getInfluenceSellToGrid().get(); - - - - //TODO: wäre das nicht der richtige Wert? G -// int minEssPower = this.ess.getAllowedChargePower().getOrError(); -// int maxEssPower = this.ess.getAllowedDischargePower().getOrError(); + long levlSocWs = this.getLevlSoc().getOrError(); + long remainingLevlEnergyWs = this.getRemainingLevlEnergy().getOrError(); + double efficiency = this.getEfficiency().getOrError(); + double socLowerBoundLevlPercent = this.getSocLowerBoundLevl().getOrError(); + double socUpperBoundLevlPercent = this.getSocUpperBoundLevl().getOrError(); + long buyFromGridLimit = this.getBuyFromGridLimit().getOrError(); + long sellToGridLimit = this.getSellToGridLimit().getOrError(); + boolean influenceSellToGrid = this.getInfluenceSellToGrid().getOrError(); int minEssPower = this.ess.getPower().getMinPower(this.ess, Phase.ALL, Pwr.ACTIVE); int maxEssPower = this.ess.getPower().getMaxPower(this.ess, Phase.ALL, Pwr.ACTIVE); @@ -353,8 +333,9 @@ protected JsonrpcResponse handleRequest(Call ca var request = LevlControlRequest.from(call.getRequest()); this.log.info("Received new levl request: {}", request); this.nextRequest = request; - this._setLevlSoc(request.levlSocWh * 3600 - this.realizedEnergyBatteryWs); - this.log.info("Updated levl soc: {}", this.getLevlSoc().get()); + var nextLevlSoc = request.levlSocWh * 3600 - this.realizedEnergyBatteryWs; + this._setLevlSoc(nextLevlSoc); + this.log.info("Updated levl soc: {}", nextLevlSoc); return JsonrpcResponseSuccess .from(this.generateResponse(call.getRequest().getId(), request.levlRequestId)); } @@ -376,7 +357,7 @@ private static boolean isActive(LevlControlRequest request) { @Override public void handleEvent(Event event) { switch (event.getTopic()) { - case EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS -> { + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE -> { if (isActive(this.nextRequest)) { if (this.currentRequest != null) { this.finishRequest(); @@ -387,31 +368,38 @@ public void handleEvent(Event event) { } } case EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE -> { - if (this.currentRequest != null) { - long levlPower = 0; - - var essNextPower = this.ess.getDebugSetActivePowerChannel().getNextValue(); - //TODO: Bisher wurde der DebugSetActivePower verwendet. In den unteren schreiben wir jedoch rein. Welcher ist der Richtige? - // In unserem Integrationstest mal beide Werte loggen. - //var essNextPower = this.ess.getSetActivePowerEqualsWithPidChannel().getNextValue(); - if (essNextPower.isDefined()) { - var essPower = essNextPower.get(); - var pucBatteryPower = this.getPucBatteryPowerChannel().getNextValue().get(); - levlPower = essPower - pucBatteryPower; - } - long levlEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); - // remaining for the NEXT calculation cycle - this._setRemainingLevlEnergy(this.getRemainingLevlEnergy().get() - levlEnergyWs); - // realized AFTER the next cycle (next second) - this.realizedEnergyGridWs += levlEnergyWs; - this.log.info("this cycle realized levl energy on grid: {}", levlEnergyWs); - double efficiency = this.getEfficiency().get(); - this.realizedEnergyBatteryWs += Efficiency.apply(levlEnergyWs, efficiency); - this._setLevlSoc(this.getLevlSoc().get() - Efficiency.apply(levlEnergyWs, efficiency)); + try { + handleAfterWriteEvent(); + } catch (Exception e) { + this.log.error("error executing after write event", e); } } } } + + private void handleAfterWriteEvent() throws OpenemsNamedException { + if (this.currentRequest != null) { + var pucBatteryPower = this.getPucBatteryPowerChannel().getNextValue().getOrError(); + var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); + var efficiency = this.getEfficiency().getOrError(); + var levlSoc = this.getLevlSoc().getOrError(); + + long levlPower = 0; + var essNextPower = this.ess.getDebugSetActivePowerChannel().getNextValue(); + if (essNextPower.isDefined()) { + var essPower = essNextPower.get(); + levlPower = essPower - pucBatteryPower; + } + long levlEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); + // remaining for the NEXT calculation cycle + this._setRemainingLevlEnergy(remainingLevlEnergy - levlEnergyWs); + // realized AFTER the next cycle (next second) + this.realizedEnergyGridWs += levlEnergyWs; + this.log.info("this cycle realized levl energy on grid: {}", levlEnergyWs); + this.realizedEnergyBatteryWs += Efficiency.apply(levlEnergyWs, efficiency); + this._setLevlSoc(levlSoc - Efficiency.apply(levlEnergyWs, efficiency)); + } + } private void finishRequest() { this.log.info("finished levl request: {}", this.currentRequest); @@ -424,8 +412,6 @@ private void finishRequest() { this.currentRequest = null; } - //TODO: Values will be active with the beginning of the next cycle, because we set the "nextValue". - // There is no way to set the current value directly. However, we could run this triggered by event "BEFORE_PROCESS_IMAGE" instead of "BEFORE_CONTROLLERS". private void startNextRequest() { this.log.info("starting levl request: {}", this.currentRequest); this.currentRequest = this.nextRequest; 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 index 39dfb0656db..435a3de7a72 100644 --- 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 @@ -17,6 +17,7 @@ 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.DummyCycle; @@ -46,21 +47,21 @@ public void testCalculateRequiredPower() throws OpenemsNamedException { this.underTest.meter = new DummyElectricityMeter("meter0").withActivePower(200); this.underTest.realizedEnergyGridWs = 100; - this.underTest._setLevlSoc(2000L); //TODO: Channels lassen sich innerhalb dieses Unit-Tests nicht setzen. Ggf. nicht notwendig, da Prüfung via OpenEMS-Test - this.underTest._setEfficiency(100.0); - this.underTest._setSocLowerBoundLevl(20.0); - this.underTest._setSocUpperBoundLevl(80.0); - this.underTest._setBuyFromGridLimit(1000L); - this.underTest._setSellToGridLimit(-1000L); - this.underTest._setInfluenceSellToGrid(true); - + this.setActiveChannelValue(this.underTest.getLevlSocChannel(), 2000L); + this.setActiveChannelValue(this.underTest.getRemainingLevlEnergyChannel(), 200000L); + this.setActiveChannelValue(this.underTest.getEfficiencyChannel(), 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(1100, result); + Assert.assertEquals(500, result); } // Primary use case calculation - @Test public void testApplyPucSocBounds() { Assert.assertEquals("good case discharge", 30, this.underTest.applyPucSocBounds(1, 100, 50, 30, 100)); @@ -82,15 +83,15 @@ public void testApplyPucSocBounds() { @Test public void testCalculatePucBatteryPower() { - Assert.assertEquals("discharge within battery limit", 70, underTest.calculatePucBatteryPower(1, 50, 20, + Assert.assertEquals("discharge within battery limit", 70, this.underTest.calculatePucBatteryPower(1, 50, 20, 1000, 500, -150, 150, 100)); - Assert.assertEquals("discharge outside battery limit", 150, underTest.calculatePucBatteryPower(1, 200, 20, + Assert.assertEquals("discharge outside battery limit", 150, this.underTest.calculatePucBatteryPower(1, 200, 20, 1000, 500, -150, 150, 100)); - Assert.assertEquals("charge outside battery limit", -150, underTest.calculatePucBatteryPower(1, -200, -20, + Assert.assertEquals("charge outside battery limit", -150, this.underTest.calculatePucBatteryPower(1, -200, -20, 1000, 500, -150, 150, 100)); -} + } - // Levl Power calculation + // Levl Power calculation @Test public void testApplyBatteryPowerLimitsToLevlPower() { Assert.assertEquals(70, this.underTest.applyBatteryPowerLimitsToLevlPower(100, 30, -100, 100)); @@ -126,10 +127,7 @@ public void testInfluenceSellToGridConstraint() { @Test public void testHandleEvent_before_currentActive() { - Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, new HashMap<>()); - - // 2024-10-24T14:00:00 - Clock clock = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); + Clock clock = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); // 2024-10-24T14:00:00 ControllerEssBalancingImpl.clock = clock; LevlControlRequest currentRequest = new LevlControlRequest(); @@ -141,6 +139,8 @@ public void testHandleEvent_before_currentActive() { nextRequest.start = LocalDateTime.of(2024, 10, 24, 14, 15, 0); nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 29, 59); this.underTest.nextRequest = nextRequest; + + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); this.underTest.handleEvent(event); @@ -149,8 +149,6 @@ public void testHandleEvent_before_currentActive() { @Test public void testHandleEvent_before_nextRequestIsActive() { - Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, new HashMap<>()); - // 2024-10-24T14:15:00 Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); ControllerEssBalancingImpl.clock = clock; @@ -167,6 +165,8 @@ public void testHandleEvent_before_nextRequestIsActive() { this.underTest.realizedEnergyGridWs = 100; this.underTest.realizedEnergyBatteryWs = 200; + + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); this.underTest.handleEvent(event); @@ -178,10 +178,7 @@ public void testHandleEvent_before_nextRequestIsActive() { @Test public void testHandleEvent_before_gapBetweenRequests() { - Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, new HashMap<>()); - - // 2024-10-24T14:15:00 - Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); + Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); // 2024-10-24T14:15:00 ControllerEssBalancingImpl.clock = clock; LevlControlRequest currentRequest = new LevlControlRequest(); @@ -196,6 +193,8 @@ public void testHandleEvent_before_gapBetweenRequests() { this.underTest.realizedEnergyGridWs = 100; this.underTest.realizedEnergyBatteryWs = 200; + + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); this.underTest.handleEvent(event); @@ -207,8 +206,6 @@ public void testHandleEvent_before_gapBetweenRequests() { @Test public void testHandleEvent_before_noNextRequest() { - Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, new HashMap<>()); - // 2024-10-24T14:15:00 Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); ControllerEssBalancingImpl.clock = clock; @@ -220,7 +217,9 @@ public void testHandleEvent_before_noNextRequest() { this.underTest.realizedEnergyGridWs = 100; this.underTest.realizedEnergyBatteryWs = 200; - + + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); + this.underTest.handleEvent(event); Assert.assertNull(this.underTest.currentRequest); @@ -231,10 +230,9 @@ public void testHandleEvent_before_noNextRequest() { @Test public void testHandleEvent_before_noRequests() { - Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, new HashMap<>()); + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); - // 2024-10-24T14:15:00 - Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); + Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); // 2024-10-24T14:15:00 ControllerEssBalancingImpl.clock = clock; this.underTest.handleEvent(event); @@ -245,24 +243,25 @@ public void testHandleEvent_before_noRequests() { @Test public void testHandleEvent_after() { - Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, new HashMap<>()); - - this.underTest.ess = new DummyManagedSymmetricEss("ess0") - .withActivePower(-100); + this.underTest.ess = new DummyManagedSymmetricEss("ess0"); this.underTest.cycle = new DummyCycle(1000); - this.underTest._setPucBatteryPower(10L); //TODO: Channels lassen sich innerhalb dieses Unit-Tests nicht setzen. Ggf. nicht notwendig, da Prüfung via OpenEMS-Test - this.underTest._setLevlSoc(40L); - LevlControlRequest currentRequest = new LevlControlRequest(); - currentRequest.efficiencyPercent = 80.0; - this.underTest.currentRequest = currentRequest; - this.underTest.realizedEnergyGridWs = 20; - this.underTest.realizedEnergyBatteryWs = 30; + 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.getEfficiencyChannel(), 80.0); + this.underTest.currentRequest = new LevlControlRequest(); + this.underTest.realizedEnergyGridWs = -20; + this.underTest.realizedEnergyBatteryWs = -30; + + Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, new HashMap<>()); this.underTest.handleEvent(event); - Assert.assertEquals(-90, this.underTest.realizedEnergyGridWs); - Assert.assertEquals(-58, this.underTest.realizedEnergyBatteryWs); - Assert.assertEquals(128, this.underTest.getLevlSoc().get().longValue()); + Assert.assertEquals(-890, this.underTest.getRemainingLevlEnergyChannel().getNextValue().get().longValue()); + Assert.assertEquals(-130, this.underTest.realizedEnergyGridWs); + Assert.assertEquals(-118, this.underTest.realizedEnergyBatteryWs); + Assert.assertEquals(128, this.underTest.getLevlSocChannel().getNextValue().get().longValue()); } @Test @@ -283,14 +282,22 @@ public void testHandleRequest() throws OpenemsNamedException { JsonrpcRequest request = new GenericJsonrpcRequest("sendLevlControlRequest", params); Call call = new Call(request); - // 2024-10-24T14:00:00 - Clock clock = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); + Clock clock = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); // 2024-10-24T14:00:00 LevlControlRequest.clock = clock; - LevlControlRequest expectedNextRequest = new LevlControlRequest(3000, 4000, "id", "2024-10-24T14:15:00Z", 500*900, LocalDateTime.of(2024, 10, 24, 14, 15, 0), LocalDateTime.of(2024, 10, 24, 14, 30, 0), 10000, 20, 80, 90, true); + LevlControlRequest expectedNextRequest = new LevlControlRequest(3000, 4000, "id", "2024-10-24T14:15:00Z", (500 * 900), LocalDateTime.of(2024, 10, 24, 14, 15, 0), LocalDateTime.of(2024, 10, 24, 14, 30, 0), 10000, 20, 80, 90, true); this.underTest.handleRequest(call); Assert.assertEquals(expectedNextRequest, this.underTest.nextRequest); - Assert.assertEquals(36000000, this.underTest.getLevlSoc().get().longValue()); //TODO: Channels lassen sich innerhalb dieses Unit-Tests nicht abfragen. Kann auch NICHT durch OpenEMS-Test abgetestet werden! + Assert.assertEquals(36000000, this.underTest.getLevlSocChannel().getNextValue().get().longValue()); + } + + public void setActiveChannelValue(AbstractReadChannel channel, Object value) { + channel.setNextValue(value); + channel.nextProcessImage(); + } + + public void setNextChannelValue(AbstractReadChannel channel, Object value) { + channel.setNextValue(value); } } From 5b6b3ed04b30d54659460dfc18fda7cab4b41aef Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Wed, 6 Nov 2024 10:43:16 +0100 Subject: [PATCH 19/41] levl-1290/1297: add comments; fix checkstyle warnings; remove unused files --- .../controller/ControllerEssBalancing.java | 6 +- .../ControllerEssBalancingImpl.java | 96 +++++++++---------- .../levl/controller/LevlControlRequest.java | 51 ++++++---- .../edge/levl/controller/RequestHandler.java | 48 ---------- .../levl/controller/common/Efficiency.java | 12 +-- .../edge/levl/controller/common/Limit.java | 84 ---------------- .../ControllerEssBalancingImplTest.java | 38 ++++---- 7 files changed, 107 insertions(+), 228 deletions(-) delete mode 100644 io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java delete mode 100644 io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Limit.java diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java index 96f71d7db02..dd19c49b4d8 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -235,8 +235,8 @@ public default Value getInfluenceSellToGrid() { public default void _setInfluenceSellToGrid(Boolean value) { this.getInfluenceSellToGridChannel().setNextValue(value); } - - /** + + /** * Returns the DoubleReadChannel for the efficiency. * @return the DoubleReadChannel */ @@ -260,7 +260,7 @@ public default void _setEfficiency(Double value) { this.getEfficiencyChannel().setNextValue(value); } - /** + /** * Returns the LongReadChannel for the PUC battery power. * @return the LongReadChannel */ diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index ea7f0864d4c..c1e129c6ed0 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -20,7 +20,6 @@ import com.google.gson.JsonObject; -import io.openems.common.exceptions.InvalidValueException; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.jsonrpc.base.JsonrpcRequest; import io.openems.common.jsonrpc.base.JsonrpcResponse; @@ -41,7 +40,7 @@ @Designate(ocd = Config.class, factory = true) @Component(name = "Controller.Levl.Symmetric.Balancing", immediate = true, configurationPolicy = ConfigurationPolicy.REQUIRE) -@EventTopics({ EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS, EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, }) +@EventTopics({ EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, }) public class ControllerEssBalancingImpl extends AbstractOpenemsComponent @@ -117,7 +116,7 @@ public void run() throws OpenemsNamedException { /* * Calculates required charge/discharge power */ - var calculatedPower = calculateRequiredPower(); + var calculatedPower = this.calculateRequiredPower(); /* * set result @@ -140,14 +139,10 @@ private void initChannelValues() { } /** - * Calculates required charge/discharge power. + * Calculates the required charge/discharge power based on primary use case (puc; e.g. self consumption optimization or peak shaving) and levl. * - * @param essPower the charge/discharge power of the - * {@link ManagedSymmetricEss} - * @param gridPower the buy-from-grid/sell-to grid power - * @param targetGridSetpoint the configured targetGridSetpoint - * @return the required power - * @throws InvalidValueException + * @return the required power for the next cycle [W] + * @throws OpenemsNamedException on error */ protected int calculateRequiredPower() throws OpenemsNamedException { double cycleTimeS = this.cycle.getCycleTime() / 1000.0; @@ -174,9 +169,9 @@ protected int calculateRequiredPower() throws OpenemsNamedException { long physicalSocWs = Math.round((physicalSoc / 100.0) * essCapacityWs); // primary use case (puc) calculation - long pucSocWs = calculatePucSoc(levlSocWs, physicalSocWs); - int pucBatteryPower = calculatePucBatteryPower(cycleTimeS, gridPower, essPower, - essCapacityWs, pucSocWs, minEssPower, maxEssPower, efficiency); + long pucSocWs = this.calculatePucSoc(physicalSocWs, levlSocWs); + int pucBatteryPower = this.calculatePucBatteryPower(gridPower, essPower, pucSocWs, + essCapacityWs, minEssPower, maxEssPower, efficiency, cycleTimeS); int pucGridPower = gridPower + essPower - pucBatteryPower; long nextPucSocWs = pucSocWs + Math.round(pucBatteryPower * cycleTimeS); @@ -193,7 +188,14 @@ protected int calculateRequiredPower() throws OpenemsNamedException { return (int) batteryPowerW; } - protected long calculatePucSoc(long levlSocWs, long physicalSocWs) { + /** + * Calculates the soc of the primary use case based on calculated physical soc and tracked levl soc. + * @param physicalSocWs the physical soc [Ws] + * @param levlSocWs the levl soc [Ws] + * + * @return the soc of the primary use case + */ + protected long calculatePucSoc(long physicalSocWs, long levlSocWs) { var pucSoc = physicalSocWs - levlSocWs; if (pucSoc < 0) { @@ -203,19 +205,21 @@ protected long calculatePucSoc(long levlSocWs, long physicalSocWs) { } /** - * Calculates the power of the primary use case (puc) + * Calculates the power of the primary use case, taking into account the ess power limits and the soc limits. + * + * @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 total 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] * - * @param cycleTimeS - * @param gridPower - * @param essPower - * @param essCapacityWs - * @param pucSocWs - * @param minEssPower - * @param maxEssPower - * @return + * @return the puc battery power for the next cycle [W] */ - protected int calculatePucBatteryPower(double cycleTimeS, int gridPower, int essPower, - long essCapacityWs, long pucSocWs, int minEssPower, int maxEssPower, double efficiency) { + protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocWs, + long essCapacityWs, int minEssPower, int maxEssPower, double efficiency, double cycleTimeS) { // calculate pucPower without any limits int pucBatteryPower = gridPower + essPower; @@ -223,21 +227,21 @@ protected int calculatePucBatteryPower(double cycleTimeS, int gridPower, int ess pucBatteryPower = Math.max(Math.min(pucBatteryPower, maxEssPower), minEssPower); // apply soc bounds - pucBatteryPower = applyPucSocBounds(cycleTimeS, essCapacityWs, pucSocWs, pucBatteryPower, efficiency); + pucBatteryPower = this.applyPucSocBounds(pucBatteryPower, pucSocWs, essCapacityWs, efficiency, cycleTimeS); return pucBatteryPower; } /** - * Checks and corrects the pucPower if it would exceed the upper or lower limits - * of the SoC. + * Checks and corrects the puc battery power if it would exceed the upper or lower limits of the soc. * - * @param cycleTimeSec - * @param essCapacityWs - * @param pucSocWs - * @param pucPower + * @param pucPower the calculated pucPower + * @param pucSocWs the soc of the puc [Ws] + * @param essCapacityWs the total ess capacity [Ws] + * @param efficiency the efficiency of the system [%] + * @param cycleTimeS the configured openems cycle time [seconds] * @return the restricted pucPower */ - protected int applyPucSocBounds(double cycleTimeS, long essCapacityWs, long pucSocWs, int pucPower, double efficiency) { + protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, double efficiency, double cycleTimeS) { long dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; long dischargeEnergyUpperBoundWs = pucSocWs; @@ -256,7 +260,7 @@ protected int applyPucSocBounds(double cycleTimeS, long essCapacityWs, long pucS return (int) Math.max(Math.min(pucPower, powerUpperBound), powerLowerBound); } - private int calculateLevlPowerW(long remainingLevlEnergyWs, int pucBatteryPower, int minEssPower, int maxEssPower, int pucGridPower, long buyFromGridLimit, long sellToGridLimit, + protected int calculateLevlPowerW(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) { long levlPower = Math.round(remainingLevlEnergyWs / (double) cycleTimeS); @@ -304,7 +308,7 @@ protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } - public long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower, boolean influenceSellToGrid) { + 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 @@ -320,15 +324,10 @@ public long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower, @Override public void buildJsonApiRoutes(JsonApiBuilder builder) { builder.handleRequest(METHOD, call -> { - return handleRequest(call); + return this.handleRequest(call); }); } - /** - * @param call - * @return - * @throws OpenemsNamedException - */ protected JsonrpcResponse handleRequest(Call call) throws OpenemsNamedException { var request = LevlControlRequest.from(call.getRequest()); this.log.info("Received new levl request: {}", request); @@ -362,14 +361,14 @@ public void handleEvent(Event event) { if (this.currentRequest != null) { this.finishRequest(); } - startNextRequest(); - } else if (currentRequest != null && !isActive(this.currentRequest)) { + this.startNextRequest(); + } else if (this.currentRequest != null && !isActive(this.currentRequest)) { this.finishRequest(); } } case EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE -> { try { - handleAfterWriteEvent(); + this.handleAfterWriteEvent(); } catch (Exception e) { this.log.error("error executing after write event", e); } @@ -380,22 +379,23 @@ public void handleEvent(Event event) { private void handleAfterWriteEvent() throws OpenemsNamedException { if (this.currentRequest != null) { var pucBatteryPower = this.getPucBatteryPowerChannel().getNextValue().getOrError(); - var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); - var efficiency = this.getEfficiency().getOrError(); - var levlSoc = this.getLevlSoc().getOrError(); - long levlPower = 0; var essNextPower = this.ess.getDebugSetActivePowerChannel().getNextValue(); if (essNextPower.isDefined()) { var essPower = essNextPower.get(); levlPower = essPower - pucBatteryPower; } + long levlEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); // remaining for the NEXT calculation cycle + + var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); this._setRemainingLevlEnergy(remainingLevlEnergy - levlEnergyWs); // realized AFTER the next cycle (next second) this.realizedEnergyGridWs += levlEnergyWs; this.log.info("this cycle realized levl energy on grid: {}", levlEnergyWs); + var efficiency = this.getEfficiency().getOrError(); + var levlSoc = this.getLevlSoc().getOrError(); this.realizedEnergyBatteryWs += Efficiency.apply(levlEnergyWs, efficiency); this._setLevlSoc(levlSoc - Efficiency.apply(levlEnergyWs, efficiency)); } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index c99d59937d4..732165c6ad6 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -2,6 +2,7 @@ import com.google.gson.JsonObject; import io.openems.common.exceptions.OpenemsError; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.jsonrpc.base.JsonrpcRequest; import java.time.Clock; @@ -53,7 +54,14 @@ public LevlControlRequest(int startDelay, int duration) { } - public static LevlControlRequest from(JsonrpcRequest request) throws OpenemsError.OpenemsNamedException { + /** + * Generates a levl control request object based on the JSON-RPC request. + * + * @param request the JSON-RPC request + * @return the levl control request + * @throws OpenemsNamedException on error + */ + public static LevlControlRequest from(JsonrpcRequest request) throws OpenemsNamedException { var params = request.getParams(); return new LevlControlRequest(params); } @@ -93,36 +101,39 @@ private void parseFields(JsonObject params) { @Override public int hashCode() { - return Objects.hash(buyFromGridLimitW, deadline, efficiencyPercent, energyWs, influenceSellToGrid, - levlRequestId, levlSocWh, sellToGridLimitW, socLowerBoundPercent, socUpperBoundPercent, start, - timestamp); + return Objects.hash(this.buyFromGridLimitW, this.deadline, this.efficiencyPercent, this.energyWs, this.influenceSellToGrid, + this.levlRequestId, this.levlSocWh, this.sellToGridLimitW, this.socLowerBoundPercent, this.socUpperBoundPercent, this.start, + this.timestamp); } @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (getClass() != obj.getClass()) + } + if (getClass() != obj.getClass()) { return false; + } LevlControlRequest other = (LevlControlRequest) obj; - return buyFromGridLimitW == other.buyFromGridLimitW - && Objects.equals(deadline, other.deadline) - && Double.doubleToLongBits(efficiencyPercent) == Double.doubleToLongBits(other.efficiencyPercent) - && energyWs == other.energyWs && influenceSellToGrid == other.influenceSellToGrid - && Objects.equals(levlRequestId, other.levlRequestId) && levlSocWh == other.levlSocWh - && sellToGridLimitW == other.sellToGridLimitW && socLowerBoundPercent == other.socLowerBoundPercent - && socUpperBoundPercent == other.socUpperBoundPercent && Objects.equals(start, other.start) - && Objects.equals(timestamp, other.timestamp); + return this.buyFromGridLimitW == other.buyFromGridLimitW + && Objects.equals(this.deadline, other.deadline) + && Double.doubleToLongBits(this.efficiencyPercent) == Double.doubleToLongBits(other.efficiencyPercent) + && this.energyWs == other.energyWs && this.influenceSellToGrid == other.influenceSellToGrid + && Objects.equals(this.levlRequestId, other.levlRequestId) && this.levlSocWh == other.levlSocWh + && this.sellToGridLimitW == other.sellToGridLimitW && this.socLowerBoundPercent == other.socLowerBoundPercent + && this.socUpperBoundPercent == other.socUpperBoundPercent && Objects.equals(this.start, other.start) + && Objects.equals(this.timestamp, other.timestamp); } @Override public String toString() { - return "LevlControlRequest [sellToGridLimitW=" + sellToGridLimitW + ", buyFromGridLimitW=" + buyFromGridLimitW - + ", levlRequestId=" + levlRequestId + ", timestamp=" + timestamp + ", energyWs=" + energyWs - + ", start=" + start + ", deadline=" + deadline + ", levlSocWh=" + levlSocWh + ", socLowerBoundPercent=" - + socLowerBoundPercent + ", socUpperBoundPercent=" + socUpperBoundPercent + ", efficiencyPercent=" - + efficiencyPercent + ", influenceSellToGrid=" + influenceSellToGrid + "]"; + return "LevlControlRequest [sellToGridLimitW=" + this.sellToGridLimitW + ", buyFromGridLimitW=" + this.buyFromGridLimitW + + ", levlRequestId=" + this.levlRequestId + ", timestamp=" + this.timestamp + ", energyWs=" + this.energyWs + + ", start=" + this.start + ", deadline=" + this.deadline + ", levlSocWh=" + this.levlSocWh + ", socLowerBoundPercent=" + + this.socLowerBoundPercent + ", socUpperBoundPercent=" + this.socUpperBoundPercent + ", efficiencyPercent=" + + this.efficiencyPercent + ", influenceSellToGrid=" + this.influenceSellToGrid + "]"; } } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java deleted file mode 100644 index 4f60b238798..00000000000 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/RequestHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.openems.edge.levl.controller; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import com.google.gson.JsonObject; - -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.jsonrpc.base.JsonrpcRequest; -import io.openems.common.jsonrpc.base.JsonrpcResponse; -import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; - -public class RequestHandler { - - private List requests; - - public RequestHandler() { - this.requests = new ArrayList<>(); - } - - public JsonrpcResponse addRequest(JsonrpcRequest request) throws OpenemsNamedException { - var levlControlRequest = LevlControlRequest.from(request); - this.requests.add(levlControlRequest); - return JsonrpcResponseSuccess - .from(this.generateResponse(request.getId(), levlControlRequest.levlRequestId)); - } - - private JsonObject generateResponse(UUID requestId, String levlRequestId) { - JsonObject response = new JsonObject(); - var result = new JsonObject(); - result.addProperty("levlRequestId", levlRequestId); - response.addProperty("id", requestId.toString()); - response.add("result", result); - return response; - } - - /** - * Returns the active request. - * - * @return the active request. Null if no request is active. - */ - // TODO implement me - protected LevlControlRequest getActiveRequest() { - return this.requests.get(0); - } - -} diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java index 1c801479c35..816c9fbb224 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java @@ -11,12 +11,12 @@ public class Efficiency { * @return the power/energy inside the battery after applying the efficiency */ public static long apply(long value, double efficiencyPercent) { - if(value <= 0) { // charge - return Math.round(value * efficiencyPercent/100); + if (value <= 0) { // charge + return Math.round(value * efficiencyPercent / 100); } // discharge - return Math.round(value / (efficiencyPercent/100)); + return Math.round(value / (efficiencyPercent / 100)); } /** @@ -28,12 +28,12 @@ public static long apply(long value, double efficiencyPercent) { * @return the power/energy outside the battery after unapplying the efficiency */ public static long unapply(long value, double efficiencyPercent) { - if(value <= 0) { // charge - return Math.round(value / (efficiencyPercent/100)); + if (value <= 0) { // charge + return Math.round(value / (efficiencyPercent / 100)); } // discharge - return Math.round(value * efficiencyPercent/100); + return Math.round(value * efficiencyPercent / 100); } } \ No newline at end of file diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Limit.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Limit.java deleted file mode 100644 index 576ccec1d16..00000000000 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Limit.java +++ /dev/null @@ -1,84 +0,0 @@ -package io.openems.edge.levl.controller.common; - -/** - * A class representing a limit with a minimum and maximum power. - */ -public record Limit(int minPower, int maxPower) { - - /** - * Returns a new Limit with the minimum and maximum power set to the minimum and maximum values of an integer. - * - * @return a new Limit - */ - public static Limit unconstrained() { - return new Limit(Integer.MIN_VALUE, Integer.MAX_VALUE); - } - - /** - * Returns a new Limit with the minimum power set to the given bound and the maximum power set to the maximum value of an integer. - * - * @param bound the minimum power - * @return a new Limit - */ - public static Limit lowerBound(int bound) { - return new Limit(bound, Integer.MAX_VALUE); - } - - /** - * Returns a new Limit with the minimum power set to the minimum value of an integer and the maximum power set to the given bound. - * - * @param bound the maximum power - * @return a new Limit - */ - public static Limit upperBound(int bound) { - return new Limit(Integer.MIN_VALUE, bound); - } - - /** - * Returns the given value constrained by the minimum and maximum power of this Limit. - * - * @param value the value to constrain - * @return the constrained value - */ - public int apply(int value) { - return Math.max(Math.min(value, this.maxPower), this.minPower); - } - - /** - * Returns a new Limit with the maximum of the minimum powers and the minimum of the maximum powers of this Limit and the given Limit. - * - * @param otherLimit the other Limit - * @return a new Limit - */ - public Limit intersect(Limit otherLimit) { - return new Limit(Math.max(this.minPower, otherLimit.minPower), Math.min(this.maxPower, otherLimit.maxPower)); - } - - /** - * Returns a new Limit with the negative of the maximum power as the minimum power and the negative of the minimum power as the maximum power. - * - * @return a new Limit - */ - public Limit invert() { - return new Limit(-this.maxPower, -this.minPower); - } - - /** - * Returns a new Limit with the minimum and maximum power of this Limit shifted by the given delta. - * - * @param delta the amount to shift by - * @return a new Limit - */ - public Limit shiftBy(int delta) { - return new Limit(this.minPower + delta, this.maxPower + delta); - } - - /** - * Returns a new Limit with the minimum power as the minimum of 0 and the minimum power of this Limit and the maximum power as the maximum of 0 and the maximum power of this Limit. - * - * @return a new Limit - */ - public Limit ensureValidLimitWithZero() { - return new Limit(Math.min(0, this.minPower), Math.max(0, this.maxPower)); - } -} \ No newline at end of file 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 index 435a3de7a72..b0ab5c0e7f8 100644 --- 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 @@ -64,31 +64,31 @@ public void testCalculateRequiredPower() throws OpenemsNamedException { // Primary use case calculation @Test public void testApplyPucSocBounds() { - Assert.assertEquals("good case discharge", 30, this.underTest.applyPucSocBounds(1, 100, 50, 30, 100)); - Assert.assertEquals("good case charge", -30, this.underTest.applyPucSocBounds(1, 100, 50, -30, 100)); - Assert.assertEquals("minimum limit applies", 50, this.underTest.applyPucSocBounds(1, 100, 50, 70, 100)); - Assert.assertEquals("minimum limit applies due to cycleTime", 25, this.underTest.applyPucSocBounds(2, 100, 50, 30, 100)); - Assert.assertEquals("maximum limit applies", -50, this.underTest.applyPucSocBounds(1, 100, 50, -70, 100)); - Assert.assertEquals("no charging allowed because soc is 100%", 0, this.underTest.applyPucSocBounds(1, 100, 100, -20, 100)); - Assert.assertEquals("no discharging allowed because soc is 0%", 0, this.underTest.applyPucSocBounds(1, 100, 0, 20, 100)); - Assert.assertEquals("discharging allowed with soc 100%", 20, this.underTest.applyPucSocBounds(1, 100, 100, 20, 100)); - Assert.assertEquals("charging allowed with soc 0%", -20, this.underTest.applyPucSocBounds(1, 100, 0, -20, 100)); + 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(1, 100, 50, 30, 80)); - Assert.assertEquals("good case charge /w efficiency", -30, this.underTest.applyPucSocBounds(1, 100, 50, -30, 80)); - Assert.assertEquals("minimum limit applies /w efficiency", 40, this.underTest.applyPucSocBounds(1, 100, 50, 70, 80)); - Assert.assertEquals("maximum limit applies /w efficiency", -62, this.underTest.applyPucSocBounds(1, 100, 50, -70, 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(1, 50, 20, - 1000, 500, -150, 150, 100)); - Assert.assertEquals("discharge outside battery limit", 150, this.underTest.calculatePucBatteryPower(1, 200, 20, - 1000, 500, -150, 150, 100)); - Assert.assertEquals("charge outside battery limit", -150, this.underTest.calculatePucBatteryPower(1, -200, -20, - 1000, 500, -150, 150, 100)); + 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 From 7f9326e49569b29061378e280307076607e89d31 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Wed, 6 Nov 2024 18:35:17 +0100 Subject: [PATCH 20/41] levl-1290/1316: add default values for lastRequestRealizedEnergyGrid and LastRequestTimestamp channel; add comments --- .../ControllerEssBalancingImpl.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index c1e129c6ed0..31c6fe9e007 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -127,6 +127,8 @@ public void run() throws OpenemsNamedException { private void initChannelValues() { + this._setLastRequestRealizedEnergyGrid(0L); + this._setLastRequestTimestamp("1970-01-02T00:00:00Z"); this._setLevlSoc(0L); this._setPucBatteryPower(0L); this._setEfficiency(100.0); @@ -260,6 +262,26 @@ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, return (int) Math.max(Math.min(pucPower, powerUpperBound), powerLowerBound); } + /** + * Calculates the battery power for the level use case considering various constraints. + * + * @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 + * @param levlSocWs the current levl soc [Ws] + * @param socLowerBoundLevlPercent the lower levl soc limit [%] + * @param socUpperBoundLevlPercent the upper levl soc limit [%] + * @param essCapacityWs the total ess capacity [Ws] + * @param influenceSellToGrid if it is 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 + */ protected int calculateLevlPowerW(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) { long levlPower = Math.round(remainingLevlEnergyWs / (double) cycleTimeS); From 78a318a7f85471b2016417a4e7fdbd3e8f0db319 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Thu, 7 Nov 2024 17:04:04 +0100 Subject: [PATCH 21/41] levl-1290/1316: log correct levl request for start; remove log as value can be analyzed in time db --- .../edge/levl/controller/ControllerEssBalancingImpl.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 31c6fe9e007..39a6cf07ada 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -409,13 +409,11 @@ private void handleAfterWriteEvent() throws OpenemsNamedException { } long levlEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); - // remaining for the NEXT calculation cycle - var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); + // remaining for the next cycle this._setRemainingLevlEnergy(remainingLevlEnergy - levlEnergyWs); - // realized AFTER the next cycle (next second) + // realized after the next cycle this.realizedEnergyGridWs += levlEnergyWs; - this.log.info("this cycle realized levl energy on grid: {}", levlEnergyWs); var efficiency = this.getEfficiency().getOrError(); var levlSoc = this.getLevlSoc().getOrError(); this.realizedEnergyBatteryWs += Efficiency.apply(levlEnergyWs, efficiency); @@ -435,7 +433,7 @@ private void finishRequest() { } private void startNextRequest() { - this.log.info("starting levl request: {}", this.currentRequest); + this.log.info("starting levl request: {}", this.nextRequest); this.currentRequest = this.nextRequest; this.nextRequest = null; this._setEfficiency(this.currentRequest.efficiencyPercent); From a1abc47583557e0442b20a346730434697e91e09 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Fri, 8 Nov 2024 08:55:22 +0100 Subject: [PATCH 22/41] levl-1290/1297: add comments --- .../ControllerEssBalancingImpl.java | 83 ++++++++++++++++--- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 39a6cf07ada..96b77ff842a 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -178,15 +178,15 @@ protected int calculateRequiredPower() throws OpenemsNamedException { long nextPucSocWs = pucSocWs + Math.round(pucBatteryPower * cycleTimeS); // levl calculation - int levlPowerW = 0; + int levlBatteryPowerW = 0; if (remainingLevlEnergyWs != 0) { - levlPowerW = this.calculateLevlPowerW(remainingLevlEnergyWs, pucBatteryPower, minEssPower, maxEssPower, pucGridPower, buyFromGridLimit, sellToGridLimit, + levlBatteryPowerW = this.calculateLevlBatteryPowerW(remainingLevlEnergyWs, pucBatteryPower, minEssPower, maxEssPower, pucGridPower, buyFromGridLimit, sellToGridLimit, nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, socUpperBoundLevlPercent, essCapacityWs, influenceSellToGrid, efficiency, cycleTimeS); } // overall calculation this._setPucBatteryPower(Long.valueOf(pucBatteryPower)); - long batteryPowerW = pucBatteryPower + levlPowerW; + long batteryPowerW = pucBatteryPower + levlBatteryPowerW; return (int) batteryPowerW; } @@ -236,12 +236,12 @@ protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocW /** * Checks and corrects the puc battery power if it would exceed the upper or lower limits of the soc. * - * @param pucPower the calculated pucPower + * @param pucPower the calculated pucPower [W] * @param pucSocWs the soc of the puc [Ws] * @param essCapacityWs the total ess capacity [Ws] * @param efficiency the efficiency of the system [%] * @param cycleTimeS the configured openems cycle time [seconds] - * @return the restricted pucPower + * @return the restricted pucPower [W] */ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, double efficiency, double cycleTimeS) { long dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; @@ -272,17 +272,17 @@ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, * @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 + * @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 total ess capacity [Ws] - * @param influenceSellToGrid if it is allowed to influence sell to grid + * @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 + * @return the levl battery power [W] */ - protected int calculateLevlPowerW(long remainingLevlEnergyWs, int pucBatteryPower, int minEssPower, int maxEssPower, int pucGridPower, long buyFromGridLimit, long sellToGridLimit, + protected int calculateLevlBatteryPowerW(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) { long levlPower = Math.round(remainingLevlEnergyWs / (double) cycleTimeS); @@ -294,13 +294,35 @@ protected int calculateLevlPowerW(long remainingLevlEnergyWs, int pucBatteryPowe return (int) levlPower; } - protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, - int maxEssPower) { + + /** + * Applies battery power limits to the levl power. + * + * @param levlPower the puc 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 levl battery power [W] + */ + protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, int maxEssPower) { long levlPowerLowerBound = Long.valueOf(minEssPower) - pucBatteryPower; long levlPowerUpperBound = Long.valueOf(maxEssPower) - pucBatteryPower; return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } + /** + * Applies upper and lower soc bounderies to the levl power. + * + * @param levlPower the puc battery power [W] + * @param nextPucSocWs the calculated puc soc for the next cycle + * @param levlSocWs the current levl soc [Ws] + * @param socLowerBoundLevlPercent the lower levl soc limit [%] + * @param socUpperBoundLevlPercent the upper levl soc limit [%] + * @param essCapacityWs the total ess capacity [Ws] + * @param efficiency the efficiency of the system [%] + * @param cycleTimeS the configured openems cycle time [seconds] + * @return the levl battery power [W] + */ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long levlSocWs, double socLowerBoundLevlPercent, double socUpperBoundLevlPercent, long essCapacityWs, double efficiency, double cycleTimeS) { long levlSocLowerBoundWs = Math.round(socLowerBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; @@ -323,13 +345,30 @@ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } - + + /** + * Applies grid power limits to the levl power. + * + * @param levlPower the puc 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 levl battery power [W] + */ protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, long buyFromGridLimit, long sellToGridLimit) { long levlPowerLowerBound = -(buyFromGridLimit - pucGridPower); long levlPowerUpperBound = -(sellToGridLimit - pucGridPower); return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } + /** + * Applies influence sell to grid constraint to the levl power. + * + * @param levlPower the puc 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 levl battery power [W] + */ protected long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower, boolean influenceSellToGrid) { if (!influenceSellToGrid) { if (pucGridPower < 0) { @@ -350,6 +389,13 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) { }); } + /** + * 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 call) throws OpenemsNamedException { var request = LevlControlRequest.from(call.getRequest()); this.log.info("Received new levl request: {}", request); @@ -398,6 +444,12 @@ public void handleEvent(Event event) { } } + /** + * Determines the levl soc based on the ess power for the next cycle. + * Updates channel and class variables for the next cycle. + * + * @throws OpenemsNamedException on error + */ private void handleAfterWriteEvent() throws OpenemsNamedException { if (this.currentRequest != null) { var pucBatteryPower = this.getPucBatteryPowerChannel().getNextValue().getOrError(); @@ -421,6 +473,10 @@ private void handleAfterWriteEvent() throws OpenemsNamedException { } } + /** + * Sets last request realized energy of the finished request. + * Reset the realized energy class values for the next active request. + */ private void finishRequest() { this.log.info("finished levl request: {}", this.currentRequest); this._setLastRequestRealizedEnergyGrid(this.realizedEnergyGridWs); @@ -432,6 +488,9 @@ private void finishRequest() { this.currentRequest = null; } + /** + * Sets the nextRequest as the current request. Updates request specific channels with request values. + */ private void startNextRequest() { this.log.info("starting levl request: {}", this.nextRequest); this.currentRequest = this.nextRequest; From e60f1f195112679bfae13df1b177c012573162d5 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Fri, 8 Nov 2024 16:01:36 +0100 Subject: [PATCH 23/41] levl-1290/1316: add channels for realized values of current request as well for realized battery value of last request --- .../controller/ControllerEssBalancing.java | 101 ++++++++++++++++-- .../ControllerEssBalancingImpl.java | 35 ++++-- .../levl/controller/BalancingImplTest.java | 4 +- .../ControllerEssBalancingImplTest.java | 41 +++---- 4 files changed, 137 insertions(+), 44 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java index dd19c49b4d8..99f52e0a1c1 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -15,7 +15,7 @@ public interface ControllerEssBalancing extends Controller, OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - REMAINING_LEVL_ENERGY(Doc.of(OpenemsType.LONG) + REMAINING_LEVL_ENERGY(Doc.of(OpenemsType.LONG) .persistencePriority(PersistencePriority.HIGH) .text("energy to be realized [Ws])")), LEVL_SOC(Doc.of(OpenemsType.LONG) @@ -49,9 +49,18 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .unit(Unit.WATT) .persistencePriority(PersistencePriority.HIGH) .text("power that is applied for the ess primary use case")), + REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG) + .persistencePriority(PersistencePriority.HIGH) + .text("energy realized for the current request on the grid [Ws])")), + REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG) + .persistencePriority(PersistencePriority.HIGH) + .text("energy realized for the current request in the battery [Ws])")), LAST_REQUEST_REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG) .persistencePriority(PersistencePriority.HIGH) - .text("cumulated amount of discharge energy that has been realized since the last discharge request [Ws]")), + .text("cumulated amount of discharge energy that has been realized since the last discharge request on the grid [Ws]")), + LAST_REQUEST_REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG) + .persistencePriority(PersistencePriority.HIGH) + .text("cumulated amount of discharge energy that has been realized since the last discharge request in the battery [Ws]")), LAST_REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING) .persistencePriority(PersistencePriority.HIGH) .text("the timestamp of the last levl control request")); @@ -285,7 +294,55 @@ public default void _setPucBatteryPower(Long value) { } /** - * Returns the LongReadChannel for the realized energy grid. + * Returns the LongReadChannel for the realized energy on the grid (current request). + * @return the LongReadChannel + */ + public default LongReadChannel getRealizedEnergyGridChannel() { + return this.channel(ChannelId.REALIZED_ENERGY_GRID); + } + + /** + * Returns the value of the realized energy on the grid (current request). + * @return the value of the realized energy on the grid (current request) + */ + public default Value getRealizedEnergyGrid() { + return this.getRealizedEnergyGridChannel().value(); + } + + /** + * Sets the next value of realized energy on the grid (current request). + * @param value the next value + */ + public default void _setRealizedEnergyGrid(Long value) { + this.getRealizedEnergyGridChannel().setNextValue(value); + } + + /** + * Returns the LongReadChannel for the realized energy in the battery (current request). + * @return the LongReadChannel + */ + public default LongReadChannel getRealizedEnergyBatteryChannel() { + return this.channel(ChannelId.REALIZED_ENERGY_BATTERY); + } + + /** + * Returns the value of the realized energy in the battery (current request). + * @return the value of the realized energy in the battery (current request) + */ + public default Value getRealizedEnergyBattery() { + return this.getRealizedEnergyBatteryChannel().value(); + } + + /** + * Sets the next value of realized energy in the battery (current request). + * @param value the next value + */ + public default void _setRealizedEnergyBattery(Long value) { + this.getRealizedEnergyBatteryChannel().setNextValue(value); + } + + /** + * Returns the LongReadChannel for the realized energy on the grid (last request). * @return the LongReadChannel */ public default LongReadChannel getLastRequestRealizedEnergyGridChannel() { @@ -293,23 +350,47 @@ public default LongReadChannel getLastRequestRealizedEnergyGridChannel() { } /** - * Returns the value of the realized energy grid. - * @return the value of the realized energy grid + * Returns the value of the realized energy on the grid (last request). + * @return the value of the realized energy on the grid (last request) */ public default Value getLastRequestRealizedEnergyGrid() { return this.getLastRequestRealizedEnergyGridChannel().value(); } /** - * Sets the next value of the realized energy grid. + * Sets the next value of the realized energy on the grid (last request). * @param value the next value */ public default void _setLastRequestRealizedEnergyGrid(Long value) { this.getLastRequestRealizedEnergyGridChannel().setNextValue(value); } + + /** + * Returns the LongReadChannel for the realized energy in the battery (last request). + * @return the LongReadChannel + */ + public default LongReadChannel getLastRequestRealizedEnergyBatteryChannel() { + return this.channel(ChannelId.LAST_REQUEST_REALIZED_ENERGY_BATTERY); + } + + /** + * Returns the value of the realized energy in the battery (last request). + * @return the value of the realized energy in the battery (last request) + */ + public default Value getLastRequestRealizedEnergyBattery() { + return this.getLastRequestRealizedEnergyBatteryChannel().value(); + } + + /** + * Sets the next value of the realized energy in the battery (last request). + * @param value the next value + */ + public default void _setLastRequestRealizedEnergyBattery(Long value) { + this.getLastRequestRealizedEnergyBatteryChannel().setNextValue(value); + } /** - * Returns the StringReadChannel for the request timestamp. + * Returns the StringReadChannel for the request timestamp (last request). * @return the StringReadChannel */ public default StringReadChannel getLastRequestTimestampChannel() { @@ -317,15 +398,15 @@ public default StringReadChannel getLastRequestTimestampChannel() { } /** - * Returns the value of the request timestamp. - * @return the value of the request timestamp + * Returns the value of the request timestamp (last request). + * @return the value of the request timestamp (last request) */ public default Value getLastRequestTimestamp() { return this.getLastRequestTimestampChannel().value(); } /** - * Sets the next value of the request timestamp. + * Sets the next value of the request timestamp (last request). * @param value the next value */ public default void _setLastRequestTimestamp(String value) { diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 96b77ff842a..ca1666d0067 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -64,8 +64,6 @@ public class ControllerEssBalancingImpl extends AbstractOpenemsComponent protected LevlControlRequest currentRequest; protected LevlControlRequest nextRequest; - protected long realizedEnergyGridWs; - protected long realizedEnergyBatteryWs; private static final String METHOD = "sendLevlControlRequest"; @@ -127,7 +125,10 @@ public void run() throws OpenemsNamedException { private void initChannelValues() { + this._setRealizedEnergyGrid(0L); + this._setRealizedEnergyBattery(0L); this._setLastRequestRealizedEnergyGrid(0L); + this._setLastRequestRealizedEnergyBattery(0L); this._setLastRequestTimestamp("1970-01-02T00:00:00Z"); this._setLevlSoc(0L); this._setPucBatteryPower(0L); @@ -400,7 +401,8 @@ protected JsonrpcResponse handleRequest(Call ca var request = LevlControlRequest.from(call.getRequest()); this.log.info("Received new levl request: {}", request); this.nextRequest = request; - var nextLevlSoc = request.levlSocWh * 3600 - this.realizedEnergyBatteryWs; + var realizedEnergyBatteryWs = this.getRealizedEnergyBattery().getOrError(); + var nextLevlSoc = request.levlSocWh * 3600 - realizedEnergyBatteryWs; this._setLevlSoc(nextLevlSoc); this.log.info("Updated levl soc: {}", nextLevlSoc); return JsonrpcResponseSuccess @@ -461,14 +463,21 @@ private void handleAfterWriteEvent() throws OpenemsNamedException { } long levlEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); - var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); + // remaining for the next cycle + var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); this._setRemainingLevlEnergy(remainingLevlEnergy - levlEnergyWs); + + // realized after the next cycle + var realizedEnergyGridWs = this.getRealizedEnergyGrid().getOrError(); + this._setRealizedEnergyGrid(realizedEnergyGridWs + levlEnergyWs); + // realized after the next cycle - this.realizedEnergyGridWs += levlEnergyWs; var efficiency = this.getEfficiency().getOrError(); + var realizedEnergyBatteryWs = this.getRealizedEnergyBattery().getOrError(); + this._setRealizedEnergyBattery(realizedEnergyBatteryWs + Efficiency.apply(levlEnergyWs, efficiency)); + var levlSoc = this.getLevlSoc().getOrError(); - this.realizedEnergyBatteryWs += Efficiency.apply(levlEnergyWs, efficiency); this._setLevlSoc(levlSoc - Efficiency.apply(levlEnergyWs, efficiency)); } } @@ -478,13 +487,17 @@ private void handleAfterWriteEvent() throws OpenemsNamedException { * Reset the realized energy class values for the next active request. */ private void finishRequest() { + var realizedEnergyGridWs = this.getRealizedEnergyGridChannel().getNextValue().orElse(0L); + var realizedEnergyBatteryWs = this.getRealizedEnergyBatteryChannel().getNextValue().orElse(0L); this.log.info("finished levl request: {}", this.currentRequest); - this._setLastRequestRealizedEnergyGrid(this.realizedEnergyGridWs); + this.log.info("realized levl energy on grid: {}", realizedEnergyGridWs); + this.log.info("realized levl energy in battery: {}", realizedEnergyBatteryWs); + + this._setLastRequestRealizedEnergyGrid(realizedEnergyGridWs); + this._setLastRequestRealizedEnergyBattery(realizedEnergyBatteryWs); this._setLastRequestTimestamp(this.currentRequest.timestamp); - this.log.info("realized levl energy on grid: {}", this.realizedEnergyGridWs); - this.log.info("realized levl energy in battery: {}", this.realizedEnergyBatteryWs); - this.realizedEnergyGridWs = 0; - this.realizedEnergyBatteryWs = 0; + this._setRealizedEnergyGrid(0L); + this._setRealizedEnergyBattery(0L); this.currentRequest = null; } 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 index 7efe7c0867f..6d2cf4c5f0d 100644 --- 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 @@ -47,9 +47,7 @@ public void testWithoutLevlRequest() throws Exception { .setPower(new DummyPower(0.3, 0.3, 0.1)) .withCapacity(500000) // 1.800.000.000 Ws .withSoc(50) // 900.000.000 Ws - .withMaxApparentPower(500000) - .withAllowedChargePower(500000) //TODO: Die Werte werden in Component nicht verwendet! Herausfinden, ob wir diese berücksichtigen müssen. - .withAllowedDischargePower(500000)) + .withMaxApparentPower(500000)) .addReference("meter", new DummyElectricityMeter(METER_ID)) .addReference("cycle", new DummyCycle(1000)) .activate(MyConfig.create() 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 index b0ab5c0e7f8..0961a5eea6c 100644 --- 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 @@ -45,8 +45,7 @@ public void testCalculateRequiredPower() throws OpenemsNamedException { .withSoc(50) // 900.000 Ws .withMaxApparentPower(500); this.underTest.meter = new DummyElectricityMeter("meter0").withActivePower(200); - - this.underTest.realizedEnergyGridWs = 100; + this.setActiveChannelValue(this.underTest.getLevlSocChannel(), 2000L); this.setActiveChannelValue(this.underTest.getRemainingLevlEnergyChannel(), 200000L); this.setActiveChannelValue(this.underTest.getEfficiencyChannel(), 100.0); @@ -163,8 +162,8 @@ public void testHandleEvent_before_nextRequestIsActive() { nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 29, 59); this.underTest.nextRequest = nextRequest; - this.underTest.realizedEnergyGridWs = 100; - this.underTest.realizedEnergyBatteryWs = 200; + this.setNextChannelValue(this.underTest.getRealizedEnergyGridChannel(), 100L); + this.setNextChannelValue(this.underTest.getRealizedEnergyBatteryChannel(), 200L); Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); @@ -172,8 +171,8 @@ public void testHandleEvent_before_nextRequestIsActive() { Assert.assertEquals(nextRequest, this.underTest.currentRequest); Assert.assertNull(this.underTest.nextRequest); - Assert.assertEquals(0, this.underTest.realizedEnergyGridWs); - Assert.assertEquals(0, this.underTest.realizedEnergyBatteryWs); + Assert.assertEquals(0, this.underTest.getRealizedEnergyGridChannel().getNextValue().get().longValue()); + Assert.assertEquals(0, this.underTest.getRealizedEnergyBatteryChannel().getNextValue().get().longValue()); } @Test @@ -191,8 +190,8 @@ public void testHandleEvent_before_gapBetweenRequests() { nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 29, 59); this.underTest.nextRequest = nextRequest; - this.underTest.realizedEnergyGridWs = 100; - this.underTest.realizedEnergyBatteryWs = 200; + this.setNextChannelValue(this.underTest.getRealizedEnergyGridChannel(), 100L); + this.setNextChannelValue(this.underTest.getRealizedEnergyBatteryChannel(), 200L); Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); @@ -200,8 +199,8 @@ public void testHandleEvent_before_gapBetweenRequests() { Assert.assertNull(this.underTest.currentRequest); Assert.assertEquals(nextRequest, this.underTest.nextRequest); - Assert.assertEquals(0, this.underTest.realizedEnergyGridWs); - Assert.assertEquals(0, this.underTest.realizedEnergyBatteryWs); + Assert.assertEquals(0, this.underTest.getRealizedEnergyGridChannel().getNextValue().get().longValue()); + Assert.assertEquals(0, this.underTest.getRealizedEnergyBatteryChannel().getNextValue().get().longValue()); } @Test @@ -215,8 +214,8 @@ public void testHandleEvent_before_noNextRequest() { currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); this.underTest.currentRequest = currentRequest; - this.underTest.realizedEnergyGridWs = 100; - this.underTest.realizedEnergyBatteryWs = 200; + this.setNextChannelValue(this.underTest.getRealizedEnergyGridChannel(), 100L); + this.setNextChannelValue(this.underTest.getRealizedEnergyBatteryChannel(), 200L); Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); @@ -224,8 +223,8 @@ public void testHandleEvent_before_noNextRequest() { Assert.assertNull(this.underTest.currentRequest); Assert.assertNull(this.underTest.nextRequest); - Assert.assertEquals(0, this.underTest.realizedEnergyGridWs); - Assert.assertEquals(0, this.underTest.realizedEnergyBatteryWs); + Assert.assertEquals(0, this.underTest.getRealizedEnergyGridChannel().getNextValue().get().longValue()); + Assert.assertEquals(0, this.underTest.getRealizedEnergyBatteryChannel().getNextValue().get().longValue()); } @Test @@ -251,16 +250,17 @@ public void testHandleEvent_after() { this.setActiveChannelValue(this.underTest.getRemainingLevlEnergyChannel(), -1000L); this.setActiveChannelValue(this.underTest.getEfficiencyChannel(), 80.0); this.underTest.currentRequest = new LevlControlRequest(); - this.underTest.realizedEnergyGridWs = -20; - this.underTest.realizedEnergyBatteryWs = -30; + + 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.realizedEnergyGridWs); - Assert.assertEquals(-118, this.underTest.realizedEnergyBatteryWs); + 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()); } @@ -285,11 +285,12 @@ public void testHandleRequest() throws OpenemsNamedException { Clock clock = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); // 2024-10-24T14:00:00 LevlControlRequest.clock = clock; LevlControlRequest expectedNextRequest = new LevlControlRequest(3000, 4000, "id", "2024-10-24T14:15:00Z", (500 * 900), LocalDateTime.of(2024, 10, 24, 14, 15, 0), LocalDateTime.of(2024, 10, 24, 14, 30, 0), 10000, 20, 80, 90, true); - + this.setActiveChannelValue(this.underTest.getRealizedEnergyBatteryChannel(), -100L); + this.underTest.handleRequest(call); Assert.assertEquals(expectedNextRequest, this.underTest.nextRequest); - Assert.assertEquals(36000000, this.underTest.getLevlSocChannel().getNextValue().get().longValue()); + Assert.assertEquals(36000100, this.underTest.getLevlSocChannel().getNextValue().get().longValue()); } public void setActiveChannelValue(AbstractReadChannel channel, Object value) { From 42a6782dd9fbf7d80a2778e69992dbeeea509d3e Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Fri, 8 Nov 2024 17:41:33 +0100 Subject: [PATCH 24/41] levl-1290/1316: add temporary logging for analysis --- .../controller/ControllerEssBalancingImpl.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index ca1666d0067..dcf59702aff 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -223,14 +223,26 @@ protected long calculatePucSoc(long physicalSocWs, long levlSocWs) { */ protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocWs, long essCapacityWs, int minEssPower, int maxEssPower, double efficiency, double cycleTimeS) { + this.log.info("### calculatePucBatteryPower ###"); + this.log.info("gridPower: " + gridPower); + this.log.info("essPower: " + essPower); + this.log.info("pucSocWs: " + pucSocWs); + this.log.info("essCapacityWs: " + essCapacityWs); + this.log.info("minEssPower: " + minEssPower); + this.log.info("maxEssPower: " + maxEssPower); + this.log.info("efficiency: " + efficiency); + this.log.info("cycleTimeS: " + cycleTimeS); // calculate pucPower without any limits int pucBatteryPower = gridPower + essPower; + this.log.info("pucBatteryPower without limits: " + pucBatteryPower); // apply ess power limits pucBatteryPower = Math.max(Math.min(pucBatteryPower, maxEssPower), minEssPower); - + this.log.info("pucBatteryPower with ess power limits: " + pucBatteryPower); + // apply soc bounds pucBatteryPower = this.applyPucSocBounds(pucBatteryPower, pucSocWs, essCapacityWs, efficiency, cycleTimeS); + this.log.info("pucBatteryPower with ess power and soc limits: " + pucBatteryPower); return pucBatteryPower; } From d5f76b4a61f0d3934fcbc8a82ec78fc58ec05b63 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Fri, 8 Nov 2024 18:20:16 +0100 Subject: [PATCH 25/41] levl-1290/1316: fix int overflow by using long in calculation of essCapacity --- .../ControllerEssBalancingImpl.java | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index dcf59702aff..392398fc917 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -148,47 +148,47 @@ private void initChannelValues() { * @throws OpenemsNamedException on error */ protected int calculateRequiredPower() throws OpenemsNamedException { - double cycleTimeS = this.cycle.getCycleTime() / 1000.0; + var cycleTimeS = this.cycle.getCycleTime() / 1000.0; // load physical values - int physicalSoc = this.ess.getSoc().getOrError(); - int gridPower = this.meter.getActivePower().getOrError(); - int essPower = this.ess.getActivePower().getOrError(); - int essCapacity = this.ess.getCapacity().getOrError(); + var physicalSoc = this.ess.getSoc().getOrError(); + var gridPower = this.meter.getActivePower().getOrError(); + var essPower = this.ess.getActivePower().getOrError(); + var essCapacity = this.ess.getCapacity().getOrError(); - long levlSocWs = this.getLevlSoc().getOrError(); - long remainingLevlEnergyWs = this.getRemainingLevlEnergy().getOrError(); - double efficiency = this.getEfficiency().getOrError(); - double socLowerBoundLevlPercent = this.getSocLowerBoundLevl().getOrError(); - double socUpperBoundLevlPercent = this.getSocUpperBoundLevl().getOrError(); - long buyFromGridLimit = this.getBuyFromGridLimit().getOrError(); - long sellToGridLimit = this.getSellToGridLimit().getOrError(); - boolean influenceSellToGrid = this.getInfluenceSellToGrid().getOrError(); + var levlSocWs = this.getLevlSoc().getOrError(); + var remainingLevlEnergyWs = this.getRemainingLevlEnergy().getOrError(); + var efficiency = this.getEfficiency().getOrError(); + var socLowerBoundLevlPercent = this.getSocLowerBoundLevl().getOrError(); + var socUpperBoundLevlPercent = this.getSocUpperBoundLevl().getOrError(); + var buyFromGridLimit = this.getBuyFromGridLimit().getOrError(); + var sellToGridLimit = this.getSellToGridLimit().getOrError(); + var influenceSellToGrid = this.getInfluenceSellToGrid().getOrError(); - int minEssPower = this.ess.getPower().getMinPower(this.ess, Phase.ALL, Pwr.ACTIVE); - int maxEssPower = this.ess.getPower().getMaxPower(this.ess, Phase.ALL, Pwr.ACTIVE); + var minEssPower = this.ess.getPower().getMinPower(this.ess, Phase.ALL, Pwr.ACTIVE); + var maxEssPower = this.ess.getPower().getMaxPower(this.ess, Phase.ALL, Pwr.ACTIVE); - long essCapacityWs = essCapacity * 3600; - long physicalSocWs = Math.round((physicalSoc / 100.0) * essCapacityWs); + var essCapacityWs = essCapacity * 3600L; + var physicalSocWs = Math.round((physicalSoc / 100.0) * essCapacityWs); // primary use case (puc) calculation - long pucSocWs = this.calculatePucSoc(physicalSocWs, levlSocWs); - int pucBatteryPower = this.calculatePucBatteryPower(gridPower, essPower, pucSocWs, + var pucSocWs = this.calculatePucSoc(physicalSocWs, levlSocWs); + var pucBatteryPower = this.calculatePucBatteryPower(gridPower, essPower, pucSocWs, essCapacityWs, minEssPower, maxEssPower, efficiency, cycleTimeS); - int pucGridPower = gridPower + essPower - pucBatteryPower; - long nextPucSocWs = pucSocWs + Math.round(pucBatteryPower * cycleTimeS); + var pucGridPower = gridPower + essPower - pucBatteryPower; + var nextPucSocWs = pucSocWs + Math.round(pucBatteryPower * cycleTimeS); // levl calculation - int levlBatteryPowerW = 0; + var levlBatteryPower = 0; if (remainingLevlEnergyWs != 0) { - levlBatteryPowerW = this.calculateLevlBatteryPowerW(remainingLevlEnergyWs, pucBatteryPower, minEssPower, maxEssPower, pucGridPower, buyFromGridLimit, sellToGridLimit, + levlBatteryPower = this.calculateLevlBatteryPowerW(remainingLevlEnergyWs, pucBatteryPower, minEssPower, maxEssPower, pucGridPower, buyFromGridLimit, sellToGridLimit, nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, socUpperBoundLevlPercent, essCapacityWs, influenceSellToGrid, efficiency, cycleTimeS); } // overall calculation this._setPucBatteryPower(Long.valueOf(pucBatteryPower)); - long batteryPowerW = pucBatteryPower + levlBatteryPowerW; - return (int) batteryPowerW; + var batteryPowerW = pucBatteryPower + levlBatteryPower; + return batteryPowerW; } /** @@ -233,7 +233,7 @@ protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocW this.log.info("efficiency: " + efficiency); this.log.info("cycleTimeS: " + cycleTimeS); // calculate pucPower without any limits - int pucBatteryPower = gridPower + essPower; + var pucBatteryPower = gridPower + essPower; this.log.info("pucBatteryPower without limits: " + pucBatteryPower); // apply ess power limits @@ -257,12 +257,12 @@ protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocW * @return the restricted pucPower [W] */ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, double efficiency, double cycleTimeS) { - long dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; - long dischargeEnergyUpperBoundWs = pucSocWs; + var dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; + var dischargeEnergyUpperBoundWs = pucSocWs; - long powerLowerBound = Efficiency.unapply(Math.round(dischargeEnergyLowerBoundWs / cycleTimeS), + var powerLowerBound = Efficiency.unapply(Math.round(dischargeEnergyLowerBoundWs / cycleTimeS), efficiency); - long powerUpperBound = Efficiency.unapply(Math.round(dischargeEnergyUpperBoundWs / cycleTimeS), + var powerUpperBound = Efficiency.unapply(Math.round(dischargeEnergyUpperBoundWs / cycleTimeS), efficiency); if (powerLowerBound > 0) { @@ -297,7 +297,7 @@ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, */ protected int calculateLevlBatteryPowerW(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) { - long levlPower = Math.round(remainingLevlEnergyWs / (double) cycleTimeS); + var levlPower = Math.round(remainingLevlEnergyWs / (double) cycleTimeS); levlPower = this.applyBatteryPowerLimitsToLevlPower(levlPower, pucBatteryPower, minEssPower, maxEssPower); levlPower = this.applySocBoundariesToLevlPower(levlPower, nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, socUpperBoundLevlPercent, essCapacityWs, efficiency, cycleTimeS); @@ -318,8 +318,8 @@ protected int calculateLevlBatteryPowerW(long remainingLevlEnergyWs, int pucBatt * @return the levl battery power [W] */ protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, int maxEssPower) { - long levlPowerLowerBound = Long.valueOf(minEssPower) - pucBatteryPower; - long levlPowerUpperBound = Long.valueOf(maxEssPower) - pucBatteryPower; + var levlPowerLowerBound = Long.valueOf(minEssPower) - pucBatteryPower; + var levlPowerUpperBound = Long.valueOf(maxEssPower) - pucBatteryPower; return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } @@ -338,8 +338,8 @@ protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBattery */ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long levlSocWs, double socLowerBoundLevlPercent, double socUpperBoundLevlPercent, long essCapacityWs, double efficiency, double cycleTimeS) { - long levlSocLowerBoundWs = Math.round(socLowerBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; - long levlSocUpperBoundWs = Math.round(socUpperBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; + var levlSocLowerBoundWs = Math.round(socLowerBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; + var levlSocUpperBoundWs = Math.round(socUpperBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; if (levlSocLowerBoundWs > 0) { levlSocLowerBoundWs = 0; @@ -348,12 +348,12 @@ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, levlSocUpperBoundWs = 0; } - long levlDischargeEnergyLowerBoundWs = -(levlSocUpperBoundWs - levlSocWs); - long levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - levlSocWs); + var levlDischargeEnergyLowerBoundWs = -(levlSocUpperBoundWs - levlSocWs); + var levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - levlSocWs); - long levlPowerLowerBound = Efficiency.unapply(Math.round(levlDischargeEnergyLowerBoundWs / cycleTimeS), + var levlPowerLowerBound = Efficiency.unapply(Math.round(levlDischargeEnergyLowerBoundWs / cycleTimeS), efficiency); - long levlPowerUpperBound = Efficiency.unapply(Math.round(levlDischargeEnergyUpperBoundWs / cycleTimeS), + var levlPowerUpperBound = Efficiency.unapply(Math.round(levlDischargeEnergyUpperBoundWs / cycleTimeS), efficiency); return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); @@ -369,8 +369,8 @@ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, * @return the levl battery power [W] */ protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, long buyFromGridLimit, long sellToGridLimit) { - long levlPowerLowerBound = -(buyFromGridLimit - pucGridPower); - long levlPowerUpperBound = -(sellToGridLimit - pucGridPower); + var levlPowerLowerBound = -(buyFromGridLimit - pucGridPower); + var levlPowerUpperBound = -(sellToGridLimit - pucGridPower); return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } @@ -474,7 +474,7 @@ private void handleAfterWriteEvent() throws OpenemsNamedException { levlPower = essPower - pucBatteryPower; } - long levlEnergyWs = Math.round(levlPower * this.cycle.getCycleTime() / 1000.0); + var levlEnergyWs = Math.round(levlPower * (this.cycle.getCycleTime() / 1000.0)); // remaining for the next cycle var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); From b223db7912c10319d4adf46c5219c608677ae877 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Mon, 11 Nov 2024 09:29:12 +0100 Subject: [PATCH 26/41] levl-1290/1297: add readme --- io.openems.edge.levl.controller/readme.adoc | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/io.openems.edge.levl.controller/readme.adoc b/io.openems.edge.levl.controller/readme.adoc index 11bf25f0227..925703c40b8 100644 --- a/io.openems.edge.levl.controller/readme.adoc +++ b/io.openems.edge.levl.controller/readme.adoc @@ -1,16 +1,17 @@ -= ESS Balancing += Levl Controller for ESS Balancing -This controls a energy storage system in self-consumption optimization mode by trying to balance the power at the grid connection point, i.e.: -- charge the battery, when production is larger than consumption (i.e. when feeding power to the grid) -- discharge the battery, when consumption is larger than production (i.e. when buying power from the grid) +This controller combines the optimization of self-consumption with electricity exchange optimization in order to market the free flexibility of the energy storage system economically. +Note: This controller can only be used in combination with an optimization contract with Levl Energy. For contact und more information please visit https://levl.energy -== Requirements - -** ManagedSymmetricEss, a controllable energy storage system -** ElectricityMeter, a meter at the grid connection point +== How it works +This controller receives charging and discharging instructions from Levl Energy and executes these taking into account local optimization and various constraints (such as SoC, grid limits, etc.). -== Additional application notes - -Above description assumes that the grid connection point should be balanced to `0 Watt`. This bevahiour is configurable using the `targetGridSetpoint` configuration parameter. +== Requirements +The following components are required to use this controller successfully: +- Contract with Levl Energy for system optimization and +- ManagedSymmetricEss - a controllable energy storage system +- ElectricityMeter - a meter at the grid connection point +== Further information +Detailed information on self-consumption optimization can be found in the OpenEMS source code. https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.controller.ess.balancing[Source Code icon:github[]] \ No newline at end of file From d56ecb36b03d487b15806ce6ee0ceff1a8f2a160 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Mon, 11 Nov 2024 12:50:19 +0100 Subject: [PATCH 27/41] levl-1290/1316: fix int overflow in next levl soc calculation in handleRequest; add logging and set log-level to debug; auto-formatting --- .../controller/ControllerEssBalancing.java | 351 ++++++++++-------- .../ControllerEssBalancingImpl.java | 284 +++++++------- .../levl/controller/LevlControlRequest.java | 186 +++++----- .../ControllerEssBalancingImplTest.java | 198 +++++----- 4 files changed, 544 insertions(+), 475 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java index 99f52e0a1c1..be89d635a3f 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -14,403 +14,428 @@ public interface ControllerEssBalancing extends Controller, OpenemsComponent { - public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - REMAINING_LEVL_ENERGY(Doc.of(OpenemsType.LONG) - .persistencePriority(PersistencePriority.HIGH) - .text("energy to be realized [Ws])")), - LEVL_SOC(Doc.of(OpenemsType.LONG) - .unit(Unit.WATT_HOURS) - .persistencePriority(PersistencePriority.HIGH) - .text("levl state of charge [Wh]")), - SELL_TO_GRID_LIMIT(Doc.of(OpenemsType.LONG) - .unit(Unit.WATT) - .persistencePriority(PersistencePriority.HIGH) - .text("maximum power that may be sold to the grid [W]")), - BUY_FROM_GRID_LIMIT(Doc.of(OpenemsType.LONG) - .unit(Unit.WATT) - .persistencePriority(PersistencePriority.HIGH) - .text("maximum power that may be bought from the grid [W]")), - SOC_LOWER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE) - .unit(Unit.PERCENT) - .persistencePriority(PersistencePriority.HIGH) - .text("lower soc bound limit levl has to respect [%]")), - SOC_UPPER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE) - .unit(Unit.PERCENT) - .persistencePriority(PersistencePriority.HIGH) - .text("upper soc bound limit levl has to respect [%]")), - INFLUENCE_SELL_TO_GRID(Doc.of(OpenemsType.BOOLEAN) - .persistencePriority(PersistencePriority.HIGH) - .text("defines if levl is allowed to influence the sell to grid power [true/false]")), - EFFICIENCY(Doc.of(OpenemsType.DOUBLE) - .unit(Unit.PERCENT) - .persistencePriority(PersistencePriority.HIGH) - .text("ess efficiency defined by levl [%]")), - PUC_BATTERY_POWER(Doc.of(OpenemsType.LONG) - .unit(Unit.WATT) - .persistencePriority(PersistencePriority.HIGH) - .text("power that is applied for the ess primary use case")), - REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG) - .persistencePriority(PersistencePriority.HIGH) - .text("energy realized for the current request on the grid [Ws])")), - REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG) - .persistencePriority(PersistencePriority.HIGH) - .text("energy realized for the current request in the battery [Ws])")), - LAST_REQUEST_REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG) - .persistencePriority(PersistencePriority.HIGH) - .text("cumulated amount of discharge energy that has been realized since the last discharge request on the grid [Ws]")), - LAST_REQUEST_REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG) - .persistencePriority(PersistencePriority.HIGH) - .text("cumulated amount of discharge energy that has been realized since the last discharge request in the battery [Ws]")), - LAST_REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING) - .persistencePriority(PersistencePriority.HIGH) - .text("the timestamp of the last levl control request")); - - private final Doc doc; - - private ChannelId(Doc doc) { - this.doc = doc; - } - - @Override - public Doc doc() { - return this.doc; - } - } - + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + REMAINING_LEVL_ENERGY(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH) + .text("energy to be realized [Ws])")), + LEVL_SOC(Doc.of(OpenemsType.LONG).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH) + .text("levl state of charge [Wh]")), + SELL_TO_GRID_LIMIT(Doc.of(OpenemsType.LONG).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH) + .text("maximum power that may be sold to the grid [W]")), + BUY_FROM_GRID_LIMIT(Doc.of(OpenemsType.LONG).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH) + .text("maximum power that may be bought from the grid [W]")), + SOC_LOWER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH) + .text("lower soc bound limit levl has to respect [%]")), + SOC_UPPER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH) + .text("upper soc bound limit levl has to respect [%]")), + INFLUENCE_SELL_TO_GRID(Doc.of(OpenemsType.BOOLEAN).persistencePriority(PersistencePriority.HIGH) + .text("defines if levl is allowed to influence the sell to grid power [true/false]")), + EFFICIENCY(Doc.of(OpenemsType.DOUBLE).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH) + .text("ess efficiency defined by levl [%]")), + PUC_BATTERY_POWER(Doc.of(OpenemsType.LONG).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH) + .text("power that is applied for the ess primary use case")), + REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH) + .text("energy realized for the current request on the grid [Ws])")), + REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH) + .text("energy realized for the current request in the battery [Ws])")), + LAST_REQUEST_REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH).text( + "cumulated amount of discharge energy that has been realized since the last discharge request on the grid [Ws]")), + LAST_REQUEST_REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH) + .text("cumulated amount of discharge energy that has been realized since the last discharge request in the battery [Ws]")), + LAST_REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING).persistencePriority(PersistencePriority.HIGH) + .text("the timestamp of the last levl control request")); + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + /** * Returns the LongReadChannel for the remaining levl energy. + * * @return the LongReadChannel */ public default LongReadChannel getRemainingLevlEnergyChannel() { - return this.channel(ChannelId.REMAINING_LEVL_ENERGY); + return this.channel(ChannelId.REMAINING_LEVL_ENERGY); } - + /** * Returns the value of the remaining levl energy. + * * @return the value of the remaining levl energy */ public default Value getRemainingLevlEnergy() { - return this.getRemainingLevlEnergyChannel().value(); + return this.getRemainingLevlEnergyChannel().value(); } - + /** * Sets the next value of the remaining levl energy. + * * @param value the next value */ public default void _setRemainingLevlEnergy(Long value) { - this.getRemainingLevlEnergyChannel().setNextValue(value); + this.getRemainingLevlEnergyChannel().setNextValue(value); } - + /** * Returns the LongReadChannel for the levl state of charge. + * * @return the LongReadChannel */ public default LongReadChannel getLevlSocChannel() { - return this.channel(ChannelId.LEVL_SOC); + return this.channel(ChannelId.LEVL_SOC); } - + /** * Returns the value of the levl state of charge. + * * @return the value of the levl state of charge */ public default Value getLevlSoc() { - return this.getLevlSocChannel().value(); + return this.getLevlSocChannel().value(); } - + /** * Sets the next value of the levl state of charge. + * * @param value the next value */ public default void _setLevlSoc(Long value) { - this.getLevlSocChannel().setNextValue(value); + this.getLevlSocChannel().setNextValue(value); } /** * Returns the LongReadChannel for the sell to grid limit. + * * @return the LongReadChannel */ public default LongReadChannel getSellToGridLimitChannel() { - return this.channel(ChannelId.SELL_TO_GRID_LIMIT); + return this.channel(ChannelId.SELL_TO_GRID_LIMIT); } - + /** * Returns the value of the sell to grid limit. + * * @return the value of the sell to grid limit */ public default Value getSellToGridLimit() { - return this.getSellToGridLimitChannel().value(); + return this.getSellToGridLimitChannel().value(); } - + /** * Sets the next value of the sell to grid limit. + * * @param value the next value */ public default void _setSellToGridLimit(Long value) { - this.getSellToGridLimitChannel().setNextValue(value); + this.getSellToGridLimitChannel().setNextValue(value); } /** * Returns the LongReadChannel for the buy from grid limit. + * * @return the LongReadChannel */ public default LongReadChannel getBuyFromGridLimitChannel() { - return this.channel(ChannelId.BUY_FROM_GRID_LIMIT); + return this.channel(ChannelId.BUY_FROM_GRID_LIMIT); } - + /** * Returns the value of the buy from grid limit. + * * @return the value of the buy from grid limit */ public default Value getBuyFromGridLimit() { - return this.getBuyFromGridLimitChannel().value(); + return this.getBuyFromGridLimitChannel().value(); } - + /** * Sets the next value of the buy from grid limit. + * * @param value the next value */ public default void _setBuyFromGridLimit(Long value) { - this.getBuyFromGridLimitChannel().setNextValue(value); + this.getBuyFromGridLimitChannel().setNextValue(value); } /** * Returns the DoubleReadChannel for the lower soc bound limit. + * * @return the DoubleReadChannel */ public default DoubleReadChannel getSocLowerBoundLevlChannel() { - return this.channel(ChannelId.SOC_LOWER_BOUND_LEVL); + return this.channel(ChannelId.SOC_LOWER_BOUND_LEVL); } - + /** * Returns the value of the lower soc bound limit. + * * @return the value of the lower soc bound limit */ public default Value getSocLowerBoundLevl() { - return this.getSocLowerBoundLevlChannel().value(); + return this.getSocLowerBoundLevlChannel().value(); } - + /** * Sets the next value of the lower soc bound limit. + * * @param value the next value */ public default void _setSocLowerBoundLevl(Double value) { - this.getSocLowerBoundLevlChannel().setNextValue(value); + this.getSocLowerBoundLevlChannel().setNextValue(value); } /** * Returns the DoubleReadChannel for the upper soc bound limit. + * * @return the DoubleReadChannel */ public default DoubleReadChannel getSocUpperBoundLevlChannel() { - return this.channel(ChannelId.SOC_UPPER_BOUND_LEVL); + return this.channel(ChannelId.SOC_UPPER_BOUND_LEVL); } - + /** * Returns the value of the upper soc bound limit. + * * @return the value of the upper soc bound limit */ public default Value getSocUpperBoundLevl() { - return this.getSocUpperBoundLevlChannel().value(); + return this.getSocUpperBoundLevlChannel().value(); } - + /** * Sets the next value of the upper soc bound limit. + * * @param value the next value */ public default void _setSocUpperBoundLevl(Double value) { - this.getSocUpperBoundLevlChannel().setNextValue(value); + this.getSocUpperBoundLevlChannel().setNextValue(value); } /** * Returns the BooleanReadChannel for the influence sell to grid. + * * @return the BooleanReadChannel */ public default BooleanReadChannel getInfluenceSellToGridChannel() { - return this.channel(ChannelId.INFLUENCE_SELL_TO_GRID); + return this.channel(ChannelId.INFLUENCE_SELL_TO_GRID); } - + /** * Returns the value of the influence sell to grid. + * * @return the value of the influence sell to grid */ public default Value getInfluenceSellToGrid() { - return this.getInfluenceSellToGridChannel().value(); + return this.getInfluenceSellToGridChannel().value(); } - + /** * Sets the next value of the influence sell to grid. + * * @param value the next value */ public default void _setInfluenceSellToGrid(Boolean value) { - this.getInfluenceSellToGridChannel().setNextValue(value); - } - - /** - * Returns the DoubleReadChannel for the efficiency. - * @return the DoubleReadChannel - */ - public default DoubleReadChannel getEfficiencyChannel() { - return this.channel(ChannelId.EFFICIENCY); - } - - /** - * Returns the value of the efficiency. - * @return the value of the efficiency - */ - public default Value getEfficiency() { - return this.getEfficiencyChannel().value(); - } - - /** - * Sets the next value of the efficiency. - * @param value the next value - */ - public default void _setEfficiency(Double value) { - this.getEfficiencyChannel().setNextValue(value); - } - + this.getInfluenceSellToGridChannel().setNextValue(value); + } + + /** + * Returns the DoubleReadChannel for the efficiency. + * + * @return the DoubleReadChannel + */ + public default DoubleReadChannel getEfficiencyChannel() { + return this.channel(ChannelId.EFFICIENCY); + } + + /** + * Returns the value of the efficiency. + * + * @return the value of the efficiency + */ + public default Value getEfficiency() { + return this.getEfficiencyChannel().value(); + } + + /** + * Sets the next value of the efficiency. + * + * @param value the next value + */ + public default void _setEfficiency(Double value) { + this.getEfficiencyChannel().setNextValue(value); + } + /** * Returns the LongReadChannel for the PUC battery power. + * * @return the LongReadChannel */ public default LongReadChannel getPucBatteryPowerChannel() { - return this.channel(ChannelId.PUC_BATTERY_POWER); + return this.channel(ChannelId.PUC_BATTERY_POWER); } - + /** * Returns the value of the PUC battery power. + * * @return the value of the PUC battery power */ public default Value getPucBatteryPower() { - return this.getPucBatteryPowerChannel().value(); + return this.getPucBatteryPowerChannel().value(); } - + /** * Sets the next value of the PUC battery power. + * * @param value the next value */ public default void _setPucBatteryPower(Long value) { - this.getPucBatteryPowerChannel().setNextValue(value); + this.getPucBatteryPowerChannel().setNextValue(value); } - + /** - * Returns the LongReadChannel for the realized energy on the grid (current request). + * Returns the LongReadChannel for the realized energy on the grid (current + * request). + * * @return the LongReadChannel */ public default LongReadChannel getRealizedEnergyGridChannel() { - return this.channel(ChannelId.REALIZED_ENERGY_GRID); + return this.channel(ChannelId.REALIZED_ENERGY_GRID); } - + /** * Returns the value of the realized energy on the grid (current request). + * * @return the value of the realized energy on the grid (current request) */ public default Value getRealizedEnergyGrid() { - return this.getRealizedEnergyGridChannel().value(); + return this.getRealizedEnergyGridChannel().value(); } - + /** * Sets the next value of realized energy on the grid (current request). + * * @param value the next value */ public default void _setRealizedEnergyGrid(Long value) { - this.getRealizedEnergyGridChannel().setNextValue(value); + this.getRealizedEnergyGridChannel().setNextValue(value); } - + /** - * Returns the LongReadChannel for the realized energy in the battery (current request). + * Returns the LongReadChannel for the realized energy in the battery (current + * request). + * * @return the LongReadChannel */ public default LongReadChannel getRealizedEnergyBatteryChannel() { - return this.channel(ChannelId.REALIZED_ENERGY_BATTERY); + return this.channel(ChannelId.REALIZED_ENERGY_BATTERY); } - + /** * Returns the value of the realized energy in the battery (current request). + * * @return the value of the realized energy in the battery (current request) */ public default Value getRealizedEnergyBattery() { - return this.getRealizedEnergyBatteryChannel().value(); + return this.getRealizedEnergyBatteryChannel().value(); } - + /** * Sets the next value of realized energy in the battery (current request). + * * @param value the next value */ public default void _setRealizedEnergyBattery(Long value) { - this.getRealizedEnergyBatteryChannel().setNextValue(value); + this.getRealizedEnergyBatteryChannel().setNextValue(value); } - + /** - * Returns the LongReadChannel for the realized energy on the grid (last request). + * Returns the LongReadChannel for the realized energy on the grid (last + * request). + * * @return the LongReadChannel */ public default LongReadChannel getLastRequestRealizedEnergyGridChannel() { - return this.channel(ChannelId.LAST_REQUEST_REALIZED_ENERGY_GRID); + return this.channel(ChannelId.LAST_REQUEST_REALIZED_ENERGY_GRID); } - + /** * Returns the value of the realized energy on the grid (last request). + * * @return the value of the realized energy on the grid (last request) */ public default Value getLastRequestRealizedEnergyGrid() { - return this.getLastRequestRealizedEnergyGridChannel().value(); + return this.getLastRequestRealizedEnergyGridChannel().value(); } - + /** * Sets the next value of the realized energy on the grid (last request). + * * @param value the next value */ public default void _setLastRequestRealizedEnergyGrid(Long value) { - this.getLastRequestRealizedEnergyGridChannel().setNextValue(value); + this.getLastRequestRealizedEnergyGridChannel().setNextValue(value); } - + /** - * Returns the LongReadChannel for the realized energy in the battery (last request). + * Returns the LongReadChannel for the realized energy in the battery (last + * request). + * * @return the LongReadChannel */ public default LongReadChannel getLastRequestRealizedEnergyBatteryChannel() { - return this.channel(ChannelId.LAST_REQUEST_REALIZED_ENERGY_BATTERY); + return this.channel(ChannelId.LAST_REQUEST_REALIZED_ENERGY_BATTERY); } - + /** * Returns the value of the realized energy in the battery (last request). + * * @return the value of the realized energy in the battery (last request) */ public default Value getLastRequestRealizedEnergyBattery() { - return this.getLastRequestRealizedEnergyBatteryChannel().value(); + return this.getLastRequestRealizedEnergyBatteryChannel().value(); } - + /** * Sets the next value of the realized energy in the battery (last request). + * * @param value the next value */ public default void _setLastRequestRealizedEnergyBattery(Long value) { - this.getLastRequestRealizedEnergyBatteryChannel().setNextValue(value); + this.getLastRequestRealizedEnergyBatteryChannel().setNextValue(value); } /** * Returns the StringReadChannel for the request timestamp (last request). + * * @return the StringReadChannel */ public default StringReadChannel getLastRequestTimestampChannel() { - return this.channel(ChannelId.LAST_REQUEST_TIMESTAMP); + return this.channel(ChannelId.LAST_REQUEST_TIMESTAMP); } - + /** * Returns the value of the request timestamp (last request). + * * @return the value of the request timestamp (last request) */ public default Value getLastRequestTimestamp() { - return this.getLastRequestTimestampChannel().value(); + return this.getLastRequestTimestampChannel().value(); } - + /** * Sets the next value of the request timestamp (last request). + * * @param value the next value */ public default void _setLastRequestTimestamp(String value) { - this.getLastRequestTimestampChannel().setNextValue(value); + this.getLastRequestTimestampChannel().setNextValue(value); } } \ No newline at end of file diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 392398fc917..7c644eb386b 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -42,7 +42,6 @@ @Component(name = "Controller.Levl.Symmetric.Balancing", immediate = true, configurationPolicy = ConfigurationPolicy.REQUIRE) @EventTopics({ EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, }) - public class ControllerEssBalancingImpl extends AbstractOpenemsComponent implements Controller, OpenemsComponent, ControllerEssBalancing, ComponentJsonApi, EventHandler { @@ -84,7 +83,7 @@ private void activate(ComponentContext context, Config config) { if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "meter", config.meter_id())) { return; } - + this.initChannelValues(); } @@ -122,8 +121,7 @@ public void run() throws OpenemsNamedException { this.ess.setActivePowerEqualsWithPid(calculatedPower); this.ess.setReactivePowerEquals(0); } - - + private void initChannelValues() { this._setRealizedEnergyGrid(0L); this._setRealizedEnergyBattery(0L); @@ -142,7 +140,8 @@ private void initChannelValues() { } /** - * Calculates the required charge/discharge power based on primary use case (puc; e.g. self consumption optimization or peak shaving) and levl. + * Calculates the required charge/discharge power based on primary use case + * (puc; e.g. self consumption optimization or peak shaving) and levl. * * @return the required power for the next cycle [W] * @throws OpenemsNamedException on error @@ -155,7 +154,7 @@ protected int calculateRequiredPower() throws OpenemsNamedException { var gridPower = this.meter.getActivePower().getOrError(); var essPower = this.ess.getActivePower().getOrError(); var essCapacity = this.ess.getCapacity().getOrError(); - + var levlSocWs = this.getLevlSoc().getOrError(); var remainingLevlEnergyWs = this.getRemainingLevlEnergy().getOrError(); var efficiency = this.getEfficiency().getOrError(); @@ -173,16 +172,18 @@ protected int calculateRequiredPower() throws OpenemsNamedException { // primary use case (puc) calculation var pucSocWs = this.calculatePucSoc(physicalSocWs, levlSocWs); - var pucBatteryPower = this.calculatePucBatteryPower(gridPower, essPower, pucSocWs, - essCapacityWs, minEssPower, maxEssPower, efficiency, cycleTimeS); + var pucBatteryPower = this.calculatePucBatteryPower(gridPower, essPower, pucSocWs, essCapacityWs, minEssPower, + maxEssPower, efficiency, cycleTimeS); var pucGridPower = gridPower + essPower - pucBatteryPower; var nextPucSocWs = pucSocWs + Math.round(pucBatteryPower * cycleTimeS); // levl calculation var levlBatteryPower = 0; if (remainingLevlEnergyWs != 0) { - levlBatteryPower = this.calculateLevlBatteryPowerW(remainingLevlEnergyWs, pucBatteryPower, minEssPower, maxEssPower, pucGridPower, buyFromGridLimit, sellToGridLimit, - nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, socUpperBoundLevlPercent, essCapacityWs, influenceSellToGrid, efficiency, cycleTimeS); + levlBatteryPower = this.calculateLevlBatteryPowerW(remainingLevlEnergyWs, pucBatteryPower, minEssPower, + maxEssPower, pucGridPower, buyFromGridLimit, sellToGridLimit, nextPucSocWs, levlSocWs, + socLowerBoundLevlPercent, socUpperBoundLevlPercent, essCapacityWs, influenceSellToGrid, efficiency, + cycleTimeS); } // overall calculation @@ -190,17 +191,19 @@ protected int calculateRequiredPower() throws OpenemsNamedException { var batteryPowerW = pucBatteryPower + levlBatteryPower; return batteryPowerW; } - + /** - * Calculates the soc of the primary use case based on calculated physical soc and tracked levl soc. + * Calculates the soc of the primary use case based on calculated physical soc + * and tracked levl soc. + * * @param physicalSocWs the physical soc [Ws] - * @param levlSocWs the levl soc [Ws] + * @param levlSocWs the levl soc [Ws] * * @return the soc of the primary use case */ protected long calculatePucSoc(long physicalSocWs, long levlSocWs) { var pucSoc = physicalSocWs - levlSocWs; - + if (pucSoc < 0) { return 0; } @@ -208,63 +211,60 @@ protected long calculatePucSoc(long physicalSocWs, long levlSocWs) { } /** - * Calculates the power of the primary use case, taking into account the ess power limits and the soc limits. + * Calculates the power of the primary use case, taking into account the ess + * power limits and the soc limits. * - * @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 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 total 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] + * @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.info("### calculatePucBatteryPower ###"); - this.log.info("gridPower: " + gridPower); - this.log.info("essPower: " + essPower); - this.log.info("pucSocWs: " + pucSocWs); - this.log.info("essCapacityWs: " + essCapacityWs); - this.log.info("minEssPower: " + minEssPower); - this.log.info("maxEssPower: " + maxEssPower); - this.log.info("efficiency: " + efficiency); - this.log.info("cycleTimeS: " + cycleTimeS); + 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.info("pucBatteryPower without limits: " + pucBatteryPower); + this.log.debug("pucBatteryPower without limits: " + pucBatteryPower); // apply ess power limits pucBatteryPower = Math.max(Math.min(pucBatteryPower, maxEssPower), minEssPower); - this.log.info("pucBatteryPower with ess power limits: " + pucBatteryPower); - + this.log.debug("pucBatteryPower with ess power limits: " + pucBatteryPower); + // apply soc bounds pucBatteryPower = this.applyPucSocBounds(pucBatteryPower, pucSocWs, essCapacityWs, efficiency, cycleTimeS); - this.log.info("pucBatteryPower with ess power and soc limits: " + pucBatteryPower); + 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. + * 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 total ess capacity [Ws] - * @param efficiency the efficiency of the system [%] - * @param cycleTimeS the configured openems cycle time [seconds] + * @param pucPower the calculated pucPower [W] + * @param pucSocWs the soc of the puc [Ws] + * @param essCapacityWs the total 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) { + protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, double efficiency, + double cycleTimeS) { var dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; var dischargeEnergyUpperBoundWs = pucSocWs; - - var powerLowerBound = Efficiency.unapply(Math.round(dischargeEnergyLowerBoundWs / cycleTimeS), - efficiency); - var powerUpperBound = Efficiency.unapply(Math.round(dischargeEnergyUpperBoundWs / cycleTimeS), - efficiency); - + + var powerLowerBound = Efficiency.unapply(Math.round(dischargeEnergyLowerBoundWs / cycleTimeS), efficiency); + var powerUpperBound = Efficiency.unapply(Math.round(dischargeEnergyUpperBoundWs / cycleTimeS), efficiency); + if (powerLowerBound > 0) { powerLowerBound = 0; } @@ -276,71 +276,99 @@ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, } /** - * Calculates the battery power for the level use case considering various constraints. + * Calculates the battery power for the level use case considering various + * constraints. * - * @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 total 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] + * @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 total 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 calculateLevlBatteryPowerW(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) { + protected int calculateLevlBatteryPowerW(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 = Math.round(remainingLevlEnergyWs / (double) cycleTimeS); + this.log.debug("Initial levlPower: " + levlPower); levlPower = this.applyBatteryPowerLimitsToLevlPower(levlPower, pucBatteryPower, minEssPower, maxEssPower); - levlPower = this.applySocBoundariesToLevlPower(levlPower, nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, socUpperBoundLevlPercent, essCapacityWs, efficiency, cycleTimeS); + 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. + * Applies battery power limits to the levl power. * - * @param levlPower the puc 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] + * @param levlPower the puc 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 levl battery power [W] */ - protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, int maxEssPower) { + protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, + int maxEssPower) { var levlPowerLowerBound = Long.valueOf(minEssPower) - pucBatteryPower; var levlPowerUpperBound = Long.valueOf(maxEssPower) - pucBatteryPower; return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } /** - * Applies upper and lower soc bounderies to the levl power. + * Applies upper and lower soc bounderies to the levl power. * - * @param levlPower the puc battery power [W] - * @param nextPucSocWs the calculated puc soc for the next cycle - * @param levlSocWs the current levl soc [Ws] - * @param socLowerBoundLevlPercent the lower levl soc limit [%] - * @param socUpperBoundLevlPercent the upper levl soc limit [%] - * @param essCapacityWs the total ess capacity [Ws] - * @param efficiency the efficiency of the system [%] - * @param cycleTimeS the configured openems cycle time [seconds] + * @param levlPower the puc battery power [W] + * @param nextPucSocWs the calculated puc soc for the next cycle + * @param levlSocWs the current levl soc [Ws] + * @param socLowerBoundLevlPercent the lower levl soc limit [%] + * @param socUpperBoundLevlPercent the upper levl soc limit [%] + * @param essCapacityWs the total ess capacity [Ws] + * @param efficiency the efficiency of the system [%] + * @param cycleTimeS the configured openems cycle time [seconds] * @return the levl battery power [W] */ - protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long levlSocWs, double socLowerBoundLevlPercent, double socUpperBoundLevlPercent, long essCapacityWs, - double efficiency, double cycleTimeS) { + protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long levlSocWs, + double socLowerBoundLevlPercent, double socUpperBoundLevlPercent, long essCapacityWs, double efficiency, + double cycleTimeS) { var levlSocLowerBoundWs = Math.round(socLowerBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; var levlSocUpperBoundWs = Math.round(socUpperBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; - + if (levlSocLowerBoundWs > 0) { levlSocLowerBoundWs = 0; } @@ -358,17 +386,18 @@ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); } - + /** - * Applies grid power limits to the levl power. + * Applies grid power limits to the levl power. * - * @param levlPower the puc 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] + * @param levlPower the puc 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 levl battery power [W] */ - protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, long buyFromGridLimit, long sellToGridLimit) { + protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, long buyFromGridLimit, + long sellToGridLimit) { var levlPowerLowerBound = -(buyFromGridLimit - pucGridPower); var levlPowerUpperBound = -(sellToGridLimit - pucGridPower); return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); @@ -394,7 +423,7 @@ protected long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPow } return levlPower; } - + @Override public void buildJsonApiRoutes(JsonApiBuilder builder) { builder.handleRequest(METHOD, call -> { @@ -403,7 +432,8 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) { } /** - * Handles an incoming levl request. Updates the levl soc based on the request levl soc. + * 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 @@ -414,11 +444,10 @@ protected JsonrpcResponse handleRequest(Call ca this.log.info("Received new levl request: {}", request); this.nextRequest = request; var realizedEnergyBatteryWs = this.getRealizedEnergyBattery().getOrError(); - var nextLevlSoc = request.levlSocWh * 3600 - realizedEnergyBatteryWs; + var nextLevlSoc = request.levlSocWh * 3600L - realizedEnergyBatteryWs; this._setLevlSoc(nextLevlSoc); this.log.info("Updated levl soc: {}", nextLevlSoc); - return JsonrpcResponseSuccess - .from(this.generateResponse(call.getRequest().getId(), request.levlRequestId)); + return JsonrpcResponseSuccess.from(this.generateResponse(call.getRequest().getId(), request.levlRequestId)); } private JsonObject generateResponse(UUID requestId, String levlRequestId) { @@ -438,33 +467,33 @@ private static boolean isActive(LevlControlRequest request) { @Override public void handleEvent(Event event) { switch (event.getTopic()) { - case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE -> { - if (isActive(this.nextRequest)) { - if (this.currentRequest != null) { - this.finishRequest(); - } - this.startNextRequest(); - } else if (this.currentRequest != null && !isActive(this.currentRequest)) { + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE -> { + if (isActive(this.nextRequest)) { + if (this.currentRequest != null) { this.finishRequest(); } + this.startNextRequest(); + } else if (this.currentRequest != null && !isActive(this.currentRequest)) { + this.finishRequest(); } - case EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE -> { - try { - this.handleAfterWriteEvent(); - } catch (Exception e) { - this.log.error("error executing after write event", e); - } + } + case EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE -> { + try { + this.handleAfterWriteEvent(); + } catch (Exception e) { + this.log.error("error executing after write event", e); } } + } } - + /** - * Determines the levl soc based on the ess power for the next cycle. - * Updates channel and class variables for the next cycle. + * Determines the levl soc based on the ess power for the next cycle. Updates + * channel and class variables for the next cycle. * * @throws OpenemsNamedException on error */ - private void handleAfterWriteEvent() throws OpenemsNamedException { + private void handleAfterWriteEvent() throws OpenemsNamedException { if (this.currentRequest != null) { var pucBatteryPower = this.getPucBatteryPowerChannel().getNextValue().getOrError(); long levlPower = 0; @@ -473,30 +502,30 @@ private void handleAfterWriteEvent() throws OpenemsNamedException { var essPower = essNextPower.get(); levlPower = essPower - pucBatteryPower; } - + var levlEnergyWs = Math.round(levlPower * (this.cycle.getCycleTime() / 1000.0)); - + // remaining for the next cycle var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); this._setRemainingLevlEnergy(remainingLevlEnergy - levlEnergyWs); - + // realized after the next cycle var realizedEnergyGridWs = this.getRealizedEnergyGrid().getOrError(); this._setRealizedEnergyGrid(realizedEnergyGridWs + levlEnergyWs); - + // realized after the next cycle var efficiency = this.getEfficiency().getOrError(); var realizedEnergyBatteryWs = this.getRealizedEnergyBattery().getOrError(); this._setRealizedEnergyBattery(realizedEnergyBatteryWs + Efficiency.apply(levlEnergyWs, efficiency)); - + var levlSoc = this.getLevlSoc().getOrError(); this._setLevlSoc(levlSoc - Efficiency.apply(levlEnergyWs, efficiency)); } } /** - * Sets last request realized energy of the finished request. - * Reset the realized energy class values for the next active request. + * Sets last request realized energy of the finished request. Reset the realized + * energy class values for the next active request. */ private void finishRequest() { var realizedEnergyGridWs = this.getRealizedEnergyGridChannel().getNextValue().orElse(0L); @@ -504,7 +533,7 @@ private void finishRequest() { this.log.info("finished levl request: {}", this.currentRequest); this.log.info("realized levl energy on grid: {}", realizedEnergyGridWs); this.log.info("realized levl energy in battery: {}", realizedEnergyBatteryWs); - + this._setLastRequestRealizedEnergyGrid(realizedEnergyGridWs); this._setLastRequestRealizedEnergyBattery(realizedEnergyBatteryWs); this._setLastRequestTimestamp(this.currentRequest.timestamp); @@ -512,9 +541,10 @@ private void finishRequest() { this._setRealizedEnergyBattery(0L); this.currentRequest = null; } - + /** - * Sets the nextRequest as the current request. Updates request specific channels with request values. + * Sets the nextRequest as the current request. Updates request specific + * channels with request values. */ private void startNextRequest() { this.log.info("starting levl request: {}", this.nextRequest); diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index 732165c6ad6..236bf3aeeb7 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -10,100 +10,99 @@ import java.util.Objects; public class LevlControlRequest { - public static final String METHOD = "sendLevlControlRequest"; - public static final int QUARTER_HOUR_SECONDS = 900; - protected static Clock clock = Clock.systemDefaultZone(); - protected int sellToGridLimitW; - protected int buyFromGridLimitW; - protected String levlRequestId; - protected String timestamp; - protected long energyWs; - protected LocalDateTime start; - protected LocalDateTime deadline; - protected int levlSocWh; - protected double socLowerBoundPercent; - protected double socUpperBoundPercent; - protected double efficiencyPercent; - protected boolean influenceSellToGrid; + public static final String METHOD = "sendLevlControlRequest"; + public static final int QUARTER_HOUR_SECONDS = 900; + protected static Clock clock = Clock.systemDefaultZone(); + protected int sellToGridLimitW; + protected int buyFromGridLimitW; + protected String levlRequestId; + protected String timestamp; + protected long energyWs; + protected LocalDateTime start; + protected LocalDateTime deadline; + protected int levlSocWh; + protected double socLowerBoundPercent; + protected double socUpperBoundPercent; + protected double efficiencyPercent; + protected boolean influenceSellToGrid; - public LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String levlRequestId, - String timestamp, long energyWs, LocalDateTime start, LocalDateTime deadline, - int levlSocWh, int socLowerBoundPercent, int socUpperBoundPercent, - double efficiencyPercent, boolean influenceSellToGrid) { - this.sellToGridLimitW = sellToGridLimitW; - this.buyFromGridLimitW = buyFromGridLimitW; - this.levlRequestId = levlRequestId; - this.timestamp = timestamp; - this.energyWs = energyWs; - this.start = start; - this.deadline = deadline; - this.levlSocWh = levlSocWh; - this.socLowerBoundPercent = socLowerBoundPercent; - this.socUpperBoundPercent = socUpperBoundPercent; - this.efficiencyPercent = efficiencyPercent; - this.influenceSellToGrid = influenceSellToGrid; - } - - public LevlControlRequest() { - - } - - public LevlControlRequest(int startDelay, int duration) { - this.start = LocalDateTime.now(LevlControlRequest.clock).plusSeconds(startDelay); - this.deadline = this.start.plusSeconds(duration); + public LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String levlRequestId, String timestamp, + long energyWs, LocalDateTime start, LocalDateTime deadline, int levlSocWh, int socLowerBoundPercent, + int socUpperBoundPercent, double efficiencyPercent, boolean influenceSellToGrid) { + this.sellToGridLimitW = sellToGridLimitW; + this.buyFromGridLimitW = buyFromGridLimitW; + this.levlRequestId = levlRequestId; + this.timestamp = timestamp; + this.energyWs = energyWs; + this.start = start; + this.deadline = deadline; + this.levlSocWh = levlSocWh; + this.socLowerBoundPercent = socLowerBoundPercent; + this.socUpperBoundPercent = socUpperBoundPercent; + this.efficiencyPercent = efficiencyPercent; + this.influenceSellToGrid = influenceSellToGrid; + } + + public LevlControlRequest() { + + } + + public LevlControlRequest(int startDelay, int duration) { + this.start = LocalDateTime.now(LevlControlRequest.clock).plusSeconds(startDelay); + this.deadline = this.start.plusSeconds(duration); - } + } - /** - * Generates a levl control request object based on the JSON-RPC request. - * - * @param request the JSON-RPC request - * @return the levl control request - * @throws OpenemsNamedException on error - */ - public static LevlControlRequest from(JsonrpcRequest request) throws OpenemsNamedException { - var params = request.getParams(); - return new LevlControlRequest(params); - } + /** + * Generates a levl control request object based on the JSON-RPC request. + * + * @param request the JSON-RPC request + * @return the levl control request + * @throws OpenemsNamedException on error + */ + public static LevlControlRequest from(JsonrpcRequest request) throws OpenemsNamedException { + var params = request.getParams(); + return new LevlControlRequest(params); + } - public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedException { - try { - this.parseFields(params); - } catch (NullPointerException e) { - throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("missing fields in request: " + e.getMessage()); - } catch (NumberFormatException e) { - throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("wrong field type in request: " + e.getMessage()); - } - if (this.efficiencyPercent < 0) { - throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be > 0"); - } - if (this.efficiencyPercent > 100) { - throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be <= 100"); - } - } + public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedException { + try { + this.parseFields(params); + } catch (NullPointerException e) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("missing fields in request: " + e.getMessage()); + } catch (NumberFormatException e) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("wrong field type in request: " + e.getMessage()); + } + if (this.efficiencyPercent < 0) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be > 0"); + } + if (this.efficiencyPercent > 100) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be <= 100"); + } + } - private void parseFields(JsonObject params) { - this.levlRequestId = params.get("levlRequestId").getAsString(); - this.timestamp = params.get("levlRequestTimestamp").getAsString(); - this.energyWs = params.get("levlPowerW").getAsLong() * QUARTER_HOUR_SECONDS; - this.start = LocalDateTime.now(LevlControlRequest.clock).plusSeconds(params.get("levlChargeDelaySec").getAsInt()); - this.deadline = this.start.plusSeconds(params.get("levlChargeDurationSec").getAsInt()); - this.levlSocWh = params.get("levlSocWh").getAsInt(); - this.socLowerBoundPercent = params.get("levlSocLowerBoundPercent").getAsDouble(); - this.socUpperBoundPercent = params.get("levlSocUpperBoundPercent").getAsDouble(); - this.sellToGridLimitW = params.get("sellToGridLimitW").getAsInt(); - this.buyFromGridLimitW = params.get("buyFromGridLimitW").getAsInt(); - this.efficiencyPercent = params.get("efficiencyPercent").getAsDouble(); - this.influenceSellToGrid = params.has("influenceSellToGrid") - ? params.get("influenceSellToGrid").getAsBoolean() - : true; - } + private void parseFields(JsonObject params) { + this.levlRequestId = params.get("levlRequestId").getAsString(); + this.timestamp = params.get("levlRequestTimestamp").getAsString(); + this.energyWs = params.get("levlPowerW").getAsLong() * QUARTER_HOUR_SECONDS; + this.start = LocalDateTime.now(LevlControlRequest.clock) + .plusSeconds(params.get("levlChargeDelaySec").getAsInt()); + this.deadline = this.start.plusSeconds(params.get("levlChargeDurationSec").getAsInt()); + this.levlSocWh = params.get("levlSocWh").getAsInt(); + this.socLowerBoundPercent = params.get("levlSocLowerBoundPercent").getAsDouble(); + this.socUpperBoundPercent = params.get("levlSocUpperBoundPercent").getAsDouble(); + this.sellToGridLimitW = params.get("sellToGridLimitW").getAsInt(); + this.buyFromGridLimitW = params.get("buyFromGridLimitW").getAsInt(); + this.efficiencyPercent = params.get("efficiencyPercent").getAsDouble(); + this.influenceSellToGrid = params.has("influenceSellToGrid") ? params.get("influenceSellToGrid").getAsBoolean() + : true; + } @Override public int hashCode() { - return Objects.hash(this.buyFromGridLimitW, this.deadline, this.efficiencyPercent, this.energyWs, this.influenceSellToGrid, - this.levlRequestId, this.levlSocWh, this.sellToGridLimitW, this.socLowerBoundPercent, this.socUpperBoundPercent, this.start, - this.timestamp); + return Objects.hash(this.buyFromGridLimitW, this.deadline, this.efficiencyPercent, this.energyWs, + this.influenceSellToGrid, this.levlRequestId, this.levlSocWh, this.sellToGridLimitW, + this.socLowerBoundPercent, this.socUpperBoundPercent, this.start, this.timestamp); } @Override @@ -118,22 +117,23 @@ public boolean equals(Object obj) { return false; } LevlControlRequest other = (LevlControlRequest) obj; - return this.buyFromGridLimitW == other.buyFromGridLimitW - && Objects.equals(this.deadline, other.deadline) + return this.buyFromGridLimitW == other.buyFromGridLimitW && Objects.equals(this.deadline, other.deadline) && Double.doubleToLongBits(this.efficiencyPercent) == Double.doubleToLongBits(other.efficiencyPercent) && this.energyWs == other.energyWs && this.influenceSellToGrid == other.influenceSellToGrid && Objects.equals(this.levlRequestId, other.levlRequestId) && this.levlSocWh == other.levlSocWh - && this.sellToGridLimitW == other.sellToGridLimitW && this.socLowerBoundPercent == other.socLowerBoundPercent + && this.sellToGridLimitW == other.sellToGridLimitW + && this.socLowerBoundPercent == other.socLowerBoundPercent && this.socUpperBoundPercent == other.socUpperBoundPercent && Objects.equals(this.start, other.start) && Objects.equals(this.timestamp, other.timestamp); } @Override public String toString() { - return "LevlControlRequest [sellToGridLimitW=" + this.sellToGridLimitW + ", buyFromGridLimitW=" + this.buyFromGridLimitW - + ", levlRequestId=" + this.levlRequestId + ", timestamp=" + this.timestamp + ", energyWs=" + this.energyWs - + ", start=" + this.start + ", deadline=" + this.deadline + ", levlSocWh=" + this.levlSocWh + ", socLowerBoundPercent=" - + this.socLowerBoundPercent + ", socUpperBoundPercent=" + this.socUpperBoundPercent + ", efficiencyPercent=" + return "LevlControlRequest [sellToGridLimitW=" + this.sellToGridLimitW + ", buyFromGridLimitW=" + + this.buyFromGridLimitW + ", levlRequestId=" + this.levlRequestId + ", timestamp=" + this.timestamp + + ", energyWs=" + this.energyWs + ", start=" + this.start + ", deadline=" + this.deadline + + ", levlSocWh=" + this.levlSocWh + ", socLowerBoundPercent=" + this.socLowerBoundPercent + + ", socUpperBoundPercent=" + this.socUpperBoundPercent + ", efficiencyPercent=" + this.efficiencyPercent + ", influenceSellToGrid=" + this.influenceSellToGrid + "]"; } } 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 index 0961a5eea6c..1163a6abfa3 100644 --- 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 @@ -26,26 +26,23 @@ 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 + 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.getEfficiencyChannel(), 100.0); @@ -54,11 +51,11 @@ public void testCalculateRequiredPower() throws OpenemsNamedException { 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 @@ -66,30 +63,38 @@ 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("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("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)); + 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)); } - @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() { @@ -106,136 +111,143 @@ public void testApplySocBoundariesToLevlPower() { 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)); + 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)); + + 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 = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); // 2024-10-24T14:00:00 ControllerEssBalancingImpl.clock = clock; - + LevlControlRequest currentRequest = new LevlControlRequest(); currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); this.underTest.currentRequest = currentRequest; - + LevlControlRequest nextRequest = new LevlControlRequest(); nextRequest.start = LocalDateTime.of(2024, 10, 24, 14, 15, 0); nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 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() { // 2024-10-24T14:15:00 Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); ControllerEssBalancingImpl.clock = clock; - + LevlControlRequest currentRequest = new LevlControlRequest(); currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); this.underTest.currentRequest = currentRequest; - + LevlControlRequest nextRequest = new LevlControlRequest(); nextRequest.start = LocalDateTime.of(2024, 10, 24, 14, 15, 0); nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 29, 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 = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); // 2024-10-24T14:15:00 ControllerEssBalancingImpl.clock = clock; - + LevlControlRequest currentRequest = new LevlControlRequest(); currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); this.underTest.currentRequest = currentRequest; - + LevlControlRequest nextRequest = new LevlControlRequest(); nextRequest.start = LocalDateTime.of(2024, 10, 24, 14, 16, 0); nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 29, 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() { // 2024-10-24T14:15:00 Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); ControllerEssBalancingImpl.clock = clock; - + LevlControlRequest currentRequest = new LevlControlRequest(); currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 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 = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); // 2024-10-24T14:15:00 ControllerEssBalancingImpl.clock = clock; - + this.underTest.handleEvent(event); - + Assert.assertNull(this.underTest.currentRequest); Assert.assertNull(this.underTest.nextRequest); } @@ -250,14 +262,14 @@ public void testHandleEvent_after() { this.setActiveChannelValue(this.underTest.getRemainingLevlEnergyChannel(), -1000L); this.setActiveChannelValue(this.underTest.getEfficiencyChannel(), 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()); @@ -267,37 +279,39 @@ public void testHandleEvent_after() { @Test public void testHandleRequest() throws OpenemsNamedException { JsonObject params = new JsonObject(); - params.addProperty("levlRequestId", "id"); - params.addProperty("levlRequestTimestamp", "2024-10-24T14:15:00Z"); - params.addProperty("levlPowerW", 500); - params.addProperty("levlChargeDelaySec", 900); - params.addProperty("levlChargeDurationSec", 900); - 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); + params.addProperty("levlRequestId", "id"); + params.addProperty("levlRequestTimestamp", "2024-10-24T14:15:00Z"); + params.addProperty("levlPowerW", 500); + params.addProperty("levlChargeDelaySec", 900); + params.addProperty("levlChargeDurationSec", 900); + 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 call = new Call(request); - + Clock clock = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); // 2024-10-24T14:00:00 LevlControlRequest.clock = clock; - LevlControlRequest expectedNextRequest = new LevlControlRequest(3000, 4000, "id", "2024-10-24T14:15:00Z", (500 * 900), LocalDateTime.of(2024, 10, 24, 14, 15, 0), LocalDateTime.of(2024, 10, 24, 14, 30, 0), 10000, 20, 80, 90, true); + LevlControlRequest expectedNextRequest = new LevlControlRequest(3000, 4000, "id", "2024-10-24T14:15:00Z", + (500 * 900), LocalDateTime.of(2024, 10, 24, 14, 15, 0), LocalDateTime.of(2024, 10, 24, 14, 30, 0), + 10000, 20, 80, 90, true); this.setActiveChannelValue(this.underTest.getRealizedEnergyBatteryChannel(), -100L); this.underTest.handleRequest(call); - + Assert.assertEquals(expectedNextRequest, this.underTest.nextRequest); Assert.assertEquals(36000100, this.underTest.getLevlSocChannel().getNextValue().get().longValue()); } - + public void setActiveChannelValue(AbstractReadChannel channel, Object value) { channel.setNextValue(value); channel.nextProcessImage(); } - + public void setNextChannelValue(AbstractReadChannel channel, Object value) { channel.setNextValue(value); } From 34ba47a238ef91d9a3718a9f2c42dec34fd1ee5e Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Mon, 11 Nov 2024 12:52:38 +0100 Subject: [PATCH 28/41] levl-1290/1297: add beta version hint to readme --- io.openems.edge.levl.controller/readme.adoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/io.openems.edge.levl.controller/readme.adoc b/io.openems.edge.levl.controller/readme.adoc index 925703c40b8..5d87c0eae78 100644 --- a/io.openems.edge.levl.controller/readme.adoc +++ b/io.openems.edge.levl.controller/readme.adoc @@ -1,5 +1,7 @@ = Levl Controller for ESS Balancing +This is a beta version. + This controller combines the optimization of self-consumption with electricity exchange optimization in order to market the free flexibility of the energy storage system economically. Note: This controller can only be used in combination with an optimization contract with Levl Energy. For contact und more information please visit https://levl.energy @@ -8,7 +10,7 @@ This controller receives charging and discharging instructions from Levl Energy == Requirements The following components are required to use this controller successfully: -- Contract with Levl Energy for system optimization and +- Contract with Levl Energy for system optimization - ManagedSymmetricEss - a controllable energy storage system - ElectricityMeter - a meter at the grid connection point From f9e940a61af760b353fbb6c75c5ca720f3dc4260 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Tue, 12 Nov 2024 18:06:48 +0100 Subject: [PATCH 29/41] levl-1290/1302: cleanup; clarify comments and readme; fix nextPucSoc calculation; fix swinging of remaining energy around zero; --- io.openems.edge.levl.controller/readme.adoc | 2 +- .../openems/edge/levl/controller/Config.java | 4 +- .../controller/ControllerEssBalancing.java | 26 +++--- .../ControllerEssBalancingImpl.java | 85 +++++++++++-------- .../levl/controller/LevlControlRequest.java | 47 +++++----- .../levl/controller/BalancingImplTest.java | 11 +-- .../edge/levl/controller/MyConfig.java | 5 -- 7 files changed, 94 insertions(+), 86 deletions(-) diff --git a/io.openems.edge.levl.controller/readme.adoc b/io.openems.edge.levl.controller/readme.adoc index 5d87c0eae78..bd009f0d5af 100644 --- a/io.openems.edge.levl.controller/readme.adoc +++ b/io.openems.edge.levl.controller/readme.adoc @@ -2,7 +2,7 @@ This is a beta version. -This controller combines the optimization of self-consumption with electricity exchange optimization in order to market the free flexibility of the energy storage system economically. +This controller combines the optimization of self-consumption with flexibility trading by levl. Note: This controller can only be used in combination with an optimization contract with Levl Energy. For contact und more information please visit https://levl.energy == How it works diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java index cda77150356..cf22d48ded0 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java @@ -5,7 +5,7 @@ @ObjectClassDefinition(// name = "Levl Controller Ess Balancing", // - description = "Optimizes the self-consumption by keeping the grid meter on zero.") + description = "Combines the optimization of self-consumption with flexibility trading by levl.") @interface Config { @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") @@ -29,5 +29,5 @@ @AttributeDefinition(name = "Meter target filter", description = "This is auto-generated by 'Meter-ID'.") String meter_target() default "(enabled=true)"; - String webconsole_configurationFactory_nameHint() default "Controller Ess Balancing [{id}]"; + String webconsole_configurationFactory_nameHint() default "Levl Controller Ess Balancing [{id}]"; } \ No newline at end of file diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java index be89d635a3f..53146507036 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -16,7 +16,7 @@ public interface ControllerEssBalancing extends Controller, OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { REMAINING_LEVL_ENERGY(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH) - .text("energy to be realized [Ws])")), + .text("energy to be realized [Ws]")), LEVL_SOC(Doc.of(OpenemsType.LONG).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH) .text("levl state of charge [Wh]")), SELL_TO_GRID_LIMIT(Doc.of(OpenemsType.LONG).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH) @@ -24,9 +24,9 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { BUY_FROM_GRID_LIMIT(Doc.of(OpenemsType.LONG).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH) .text("maximum power that may be bought from the grid [W]")), SOC_LOWER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH) - .text("lower soc bound limit levl has to respect [%]")), + .text("lower soc bound levl has to respect [%]")), SOC_UPPER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH) - .text("upper soc bound limit levl has to respect [%]")), + .text("upper soc bound levl has to respect [%]")), INFLUENCE_SELL_TO_GRID(Doc.of(OpenemsType.BOOLEAN).persistencePriority(PersistencePriority.HIGH) .text("defines if levl is allowed to influence the sell to grid power [true/false]")), EFFICIENCY(Doc.of(OpenemsType.DOUBLE).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH) @@ -38,9 +38,9 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH) .text("energy realized for the current request in the battery [Ws])")), LAST_REQUEST_REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH).text( - "cumulated amount of discharge energy that has been realized since the last discharge request on the grid [Ws]")), + "energy that has been realized for the last request on the grid [Ws]")), LAST_REQUEST_REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH) - .text("cumulated amount of discharge energy that has been realized since the last discharge request in the battery [Ws]")), + .text("energy that has been realized for the last request in the battery [Ws]")), LAST_REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING).persistencePriority(PersistencePriority.HIGH) .text("the timestamp of the last levl control request")); @@ -165,7 +165,7 @@ public default void _setBuyFromGridLimit(Long value) { } /** - * Returns the DoubleReadChannel for the lower soc bound limit. + * Returns the DoubleReadChannel for the lower soc bound. * * @return the DoubleReadChannel */ @@ -174,16 +174,16 @@ public default DoubleReadChannel getSocLowerBoundLevlChannel() { } /** - * Returns the value of the lower soc bound limit. + * Returns the value of the lower soc bound. * - * @return the value of the lower soc bound limit + * @return the value of the lower soc bound */ public default Value getSocLowerBoundLevl() { return this.getSocLowerBoundLevlChannel().value(); } /** - * Sets the next value of the lower soc bound limit. + * Sets the next value of the lower soc bound. * * @param value the next value */ @@ -192,7 +192,7 @@ public default void _setSocLowerBoundLevl(Double value) { } /** - * Returns the DoubleReadChannel for the upper soc bound limit. + * Returns the DoubleReadChannel for the upper soc bound. * * @return the DoubleReadChannel */ @@ -201,16 +201,16 @@ public default DoubleReadChannel getSocUpperBoundLevlChannel() { } /** - * Returns the value of the upper soc bound limit. + * Returns the value of the upper soc bound. * - * @return the value of the upper soc bound limit + * @return the value of the upper soc bound */ public default Value getSocUpperBoundLevl() { return this.getSocUpperBoundLevlChannel().value(); } /** - * Sets the next value of the upper soc bound limit. + * Sets the next value of the upper soc bound. * * @param value the next value */ diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 7c644eb386b..18a703bb878 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -141,7 +141,7 @@ private void initChannelValues() { /** * Calculates the required charge/discharge power based on primary use case - * (puc; e.g. self consumption optimization or peak shaving) and levl. + * (puc; self consumption optimization) and levl. * * @return the required power for the next cycle [W] * @throws OpenemsNamedException on error @@ -154,7 +154,10 @@ protected int calculateRequiredPower() throws OpenemsNamedException { var gridPower = this.meter.getActivePower().getOrError(); var essPower = this.ess.getActivePower().getOrError(); var essCapacity = this.ess.getCapacity().getOrError(); + var minEssPower = this.ess.getPower().getMinPower(this.ess, Phase.ALL, Pwr.ACTIVE); + var maxEssPower = this.ess.getPower().getMaxPower(this.ess, Phase.ALL, Pwr.ACTIVE); + // levl request specific values var levlSocWs = this.getLevlSoc().getOrError(); var remainingLevlEnergyWs = this.getRemainingLevlEnergy().getOrError(); var efficiency = this.getEfficiency().getOrError(); @@ -164,23 +167,20 @@ protected int calculateRequiredPower() throws OpenemsNamedException { var sellToGridLimit = this.getSellToGridLimit().getOrError(); var influenceSellToGrid = this.getInfluenceSellToGrid().getOrError(); - var minEssPower = this.ess.getPower().getMinPower(this.ess, Phase.ALL, Pwr.ACTIVE); - var maxEssPower = this.ess.getPower().getMaxPower(this.ess, Phase.ALL, Pwr.ACTIVE); - var essCapacityWs = essCapacity * 3600L; var physicalSocWs = Math.round((physicalSoc / 100.0) * essCapacityWs); // primary use case (puc) calculation - var pucSocWs = this.calculatePucSoc(physicalSocWs, levlSocWs); + var pucSocWs = this.calculatePucSoc(physicalSocWs, levlSocWs, essCapacityWs); var pucBatteryPower = this.calculatePucBatteryPower(gridPower, essPower, pucSocWs, essCapacityWs, minEssPower, maxEssPower, efficiency, cycleTimeS); var pucGridPower = gridPower + essPower - pucBatteryPower; - var nextPucSocWs = pucSocWs + Math.round(pucBatteryPower * cycleTimeS); + var nextPucSocWs = pucSocWs - Math.round(Efficiency.apply(pucBatteryPower, efficiency) * cycleTimeS); // levl calculation var levlBatteryPower = 0; if (remainingLevlEnergyWs != 0) { - levlBatteryPower = this.calculateLevlBatteryPowerW(remainingLevlEnergyWs, pucBatteryPower, minEssPower, + levlBatteryPower = this.calculateLevlBatteryPower(remainingLevlEnergyWs, pucBatteryPower, minEssPower, maxEssPower, pucGridPower, buyFromGridLimit, sellToGridLimit, nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, socUpperBoundLevlPercent, essCapacityWs, influenceSellToGrid, efficiency, cycleTimeS); @@ -188,24 +188,28 @@ protected int calculateRequiredPower() throws OpenemsNamedException { // overall calculation this._setPucBatteryPower(Long.valueOf(pucBatteryPower)); - var batteryPowerW = pucBatteryPower + levlBatteryPower; - return batteryPowerW; + var batteryPower = pucBatteryPower + levlBatteryPower; + return batteryPower; } /** - * Calculates the soc of the primary use case based on calculated physical soc - * and tracked levl soc. + * Calculates the soc of the primary use case based on physical soc and tracked + * levl soc. * * @param physicalSocWs the physical soc [Ws] * @param levlSocWs the levl soc [Ws] + * @param essCapacityWs the ess capacity [Ws] * * @return the soc of the primary use case */ - protected long calculatePucSoc(long physicalSocWs, long levlSocWs) { + protected long calculatePucSoc(long physicalSocWs, long levlSocWs, long essCapacityWs) { var pucSoc = physicalSocWs - levlSocWs; + // handle case of pucSoc out of bounds (e.g. due to rounding) if (pucSoc < 0) { return 0; + } else if (pucSoc > essCapacityWs) { + return essCapacityWs; } return pucSoc; } @@ -214,10 +218,13 @@ protected long calculatePucSoc(long physicalSocWs, long levlSocWs) { * Calculates the power of the primary use case, taking into account the ess * power limits and the soc limits. * + * 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 total ess capacity [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 [%] @@ -252,7 +259,7 @@ protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocW * * @param pucPower the calculated pucPower [W] * @param pucSocWs the soc of the puc [Ws] - * @param essCapacityWs the total ess capacity [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] @@ -276,7 +283,7 @@ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, } /** - * Calculates the battery power for the level use case considering various + * Calculates the battery power for the levl use case considering various * constraints. * * @param remainingLevlEnergyWs the remaining energy that has to be realized @@ -294,14 +301,14 @@ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, * @param levlSocWs the current levl soc [Ws] * @param socLowerBoundLevlPercent the lower levl soc limit [%] * @param socUpperBoundLevlPercent the upper levl soc limit [%] - * @param essCapacityWs the total ess capacity [Ws] + * @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 calculateLevlBatteryPowerW(long remainingLevlEnergyWs, int pucBatteryPower, int minEssPower, + 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) { @@ -337,11 +344,11 @@ protected int calculateLevlBatteryPowerW(long remainingLevlEnergyWs, int pucBatt /** * Applies battery power limits to the levl power. * - * @param levlPower the puc battery power [W] + * @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 levl battery power [W] + * @return the restricted levl battery power [W] */ protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, int maxEssPower) { @@ -353,15 +360,15 @@ protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBattery /** * Applies upper and lower soc bounderies to the levl power. * - * @param levlPower the puc battery power [W] - * @param nextPucSocWs the calculated puc soc for the next cycle + * @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 total ess capacity [Ws] + * @param essCapacityWs the ess capacity [Ws] * @param efficiency the efficiency of the system [%] * @param cycleTimeS the configured openems cycle time [seconds] - * @return the levl battery power [W] + * @return the restricted levl battery power [W] */ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long levlSocWs, double socLowerBoundLevlPercent, double socUpperBoundLevlPercent, long essCapacityWs, double efficiency, @@ -390,11 +397,11 @@ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, /** * Applies grid power limits to the levl power. * - * @param levlPower the puc battery power [W] + * @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 levl battery power [W] + * @return the restricted levl battery power [W] */ protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, long buyFromGridLimit, long sellToGridLimit) { @@ -406,10 +413,10 @@ protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, /** * Applies influence sell to grid constraint to the levl power. * - * @param levlPower the puc battery power [W] + * @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 levl battery power [W] + * @return the restricted levl battery power [W] */ protected long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower, boolean influenceSellToGrid) { if (!influenceSellToGrid) { @@ -444,9 +451,9 @@ protected JsonrpcResponse handleRequest(Call ca this.log.info("Received new levl request: {}", request); this.nextRequest = request; var realizedEnergyBatteryWs = this.getRealizedEnergyBattery().getOrError(); - var nextLevlSoc = request.levlSocWh * 3600L - realizedEnergyBatteryWs; - this._setLevlSoc(nextLevlSoc); - this.log.info("Updated levl soc: {}", nextLevlSoc); + var updatedLevlSoc = request.levlSocWh * 3600L - realizedEnergyBatteryWs; + this._setLevlSoc(updatedLevlSoc); + this.log.info("Updated levl soc: {}", updatedLevlSoc); return JsonrpcResponseSuccess.from(this.generateResponse(call.getRequest().getId(), request.levlRequestId)); } @@ -494,9 +501,11 @@ public void handleEvent(Event event) { * @throws OpenemsNamedException on error */ private void handleAfterWriteEvent() throws OpenemsNamedException { - if (this.currentRequest != null) { + var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); + + if (remainingLevlEnergy != 0L) { var pucBatteryPower = this.getPucBatteryPowerChannel().getNextValue().getOrError(); - long levlPower = 0; + var levlPower = 0L; var essNextPower = this.ess.getDebugSetActivePowerChannel().getNextValue(); if (essNextPower.isDefined()) { var essPower = essNextPower.get(); @@ -506,8 +515,11 @@ private void handleAfterWriteEvent() throws OpenemsNamedException { var levlEnergyWs = Math.round(levlPower * (this.cycle.getCycleTime() / 1000.0)); // remaining for the next cycle - var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); - this._setRemainingLevlEnergy(remainingLevlEnergy - levlEnergyWs); + var newRemainingLevlEnergy = remainingLevlEnergy - levlEnergyWs; + if (hasSignChanged(remainingLevlEnergy, newRemainingLevlEnergy)) { + newRemainingLevlEnergy = 0; + } + this._setRemainingLevlEnergy(newRemainingLevlEnergy); // realized after the next cycle var realizedEnergyGridWs = this.getRealizedEnergyGrid().getOrError(); @@ -537,6 +549,7 @@ private void finishRequest() { this._setLastRequestRealizedEnergyGrid(realizedEnergyGridWs); this._setLastRequestRealizedEnergyBattery(realizedEnergyBatteryWs); this._setLastRequestTimestamp(this.currentRequest.timestamp); + this._setRemainingLevlEnergy(0L); this._setRealizedEnergyGrid(0L); this._setRealizedEnergyBattery(0L); this.currentRequest = null; @@ -559,4 +572,8 @@ private void startNextRequest() { this._setInfluenceSellToGrid(this.currentRequest.influenceSellToGrid); } + + private boolean hasSignChanged(long a, long b) { + return a < 0 && b > 0 || a > 0 && b < 0; + } } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index 236bf3aeeb7..7d12c18fa47 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -25,7 +25,29 @@ public class LevlControlRequest { protected double socUpperBoundPercent; protected double efficiencyPercent; protected boolean influenceSellToGrid; + + public LevlControlRequest() { + + } + + + public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedException { + try { + this.parseFields(params); + } catch (NullPointerException e) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("missing fields in request: " + e.getMessage()); + } catch (NumberFormatException e) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("wrong field type in request: " + e.getMessage()); + } + if (this.efficiencyPercent < 0) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be > 0"); + } + if (this.efficiencyPercent > 100) { + throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be <= 100"); + } + } + //Just for testing public LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String levlRequestId, String timestamp, long energyWs, LocalDateTime start, LocalDateTime deadline, int levlSocWh, int socLowerBoundPercent, int socUpperBoundPercent, double efficiencyPercent, boolean influenceSellToGrid) { @@ -43,14 +65,10 @@ public LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String le this.influenceSellToGrid = influenceSellToGrid; } - public LevlControlRequest() { - - } - + //Just for testing public LevlControlRequest(int startDelay, int duration) { this.start = LocalDateTime.now(LevlControlRequest.clock).plusSeconds(startDelay); this.deadline = this.start.plusSeconds(duration); - } /** @@ -65,22 +83,6 @@ public static LevlControlRequest from(JsonrpcRequest request) throws OpenemsName return new LevlControlRequest(params); } - public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedException { - try { - this.parseFields(params); - } catch (NullPointerException e) { - throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("missing fields in request: " + e.getMessage()); - } catch (NumberFormatException e) { - throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("wrong field type in request: " + e.getMessage()); - } - if (this.efficiencyPercent < 0) { - throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be > 0"); - } - if (this.efficiencyPercent > 100) { - throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be <= 100"); - } - } - private void parseFields(JsonObject params) { this.levlRequestId = params.get("levlRequestId").getAsString(); this.timestamp = params.get("levlRequestTimestamp").getAsString(); @@ -94,8 +96,7 @@ private void parseFields(JsonObject params) { this.sellToGridLimitW = params.get("sellToGridLimitW").getAsInt(); this.buyFromGridLimitW = params.get("buyFromGridLimitW").getAsInt(); this.efficiencyPercent = params.get("efficiencyPercent").getAsDouble(); - this.influenceSellToGrid = params.has("influenceSellToGrid") ? params.get("influenceSellToGrid").getAsBoolean() - : true; + this.influenceSellToGrid = params.get("influenceSellToGrid").getAsBoolean(); } @Override 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 index 6d2cf4c5f0d..f07afd7d7d1 100644 --- 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 @@ -12,23 +12,18 @@ 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 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, "LevlSoc"); private static final ChannelAddress LEVL_SELL_TO_GRID_LIMIT = new ChannelAddress(CTRL_ID, "SellToGridLimit"); diff --git a/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/MyConfig.java b/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/MyConfig.java index 575d315f11e..f7bcda5f25d 100644 --- a/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/MyConfig.java +++ b/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/MyConfig.java @@ -31,11 +31,6 @@ public Builder setMeterId(String meterId) { return this; } - public Builder setTargetGridSetpoint(int targetGridSetpoint) { - this.targetGridSetpoint = targetGridSetpoint; - return this; - } - public MyConfig build() { return new MyConfig(this); } From 72148eecbf81eab2ec8d4b7c7e755e2dcc001ddb Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Tue, 12 Nov 2024 18:15:52 +0100 Subject: [PATCH 30/41] levl-1290/1302: fix checkstyle --- .../levl/controller/ControllerEssBalancingImpl.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 18a703bb878..4a17ad188cc 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -217,9 +217,10 @@ protected long calculatePucSoc(long physicalSocWs, long levlSocWs, long essCapac /** * Calculates the power of the primary use case, taking into account the ess * power limits and the soc limits. - * + *

* positive = discharge * negative = charge + *

* * @param gridPower the active power of the meter [W] * @param essPower the active power of the ess [W] @@ -285,6 +286,10 @@ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, /** * 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] @@ -516,7 +521,7 @@ private void handleAfterWriteEvent() throws OpenemsNamedException { // remaining for the next cycle var newRemainingLevlEnergy = remainingLevlEnergy - levlEnergyWs; - if (hasSignChanged(remainingLevlEnergy, newRemainingLevlEnergy)) { + if (this.hasSignChanged(remainingLevlEnergy, newRemainingLevlEnergy)) { newRemainingLevlEnergy = 0; } this._setRemainingLevlEnergy(newRemainingLevlEnergy); @@ -572,7 +577,6 @@ private void startNextRequest() { this._setInfluenceSellToGrid(this.currentRequest.influenceSellToGrid); } - private boolean hasSignChanged(long a, long b) { return a < 0 && b > 0 || a > 0 && b < 0; } From 799795189b9ca347628a1a3b1cc159eddf948c70 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Wed, 13 Nov 2024 10:02:10 +0100 Subject: [PATCH 31/41] levl-1290/1302: add tests for soc limit edge cases --- .../levl/controller/BalancingImplTest.java | 202 +++++++++++++++--- 1 file changed, 174 insertions(+), 28 deletions(-) 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 index f07afd7d7d1..79c3d2b8bf7 100644 --- 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 @@ -32,7 +32,7 @@ public class BalancingImplTest { private static final ChannelAddress SOC_UPPER_BOUND_LEVL = new ChannelAddress(CTRL_ID, "SocUpperBoundLevl"); private static final ChannelAddress LEVL_INFLUENCE_SELL_TO_GRID = new ChannelAddress(CTRL_ID, "InfluenceSellToGrid"); private static final ChannelAddress LEVL_EFFICIENCY = new ChannelAddress(CTRL_ID, "Efficiency"); - private static final ChannelAddress LEVL_PUC_BATTERY_POWER = new ChannelAddress(CTRL_ID, "PucBatteryPower"); + private static final ChannelAddress PUC_BATTERY_POWER = new ChannelAddress(CTRL_ID, "PucBatteryPower"); @Test public void testWithoutLevlRequest() throws Exception { @@ -141,7 +141,7 @@ public void testWithLevlDischargeRequest() throws Exception { .input(METER_ACTIVE_POWER, 20000) .input(DEBUG_SET_ACTIVE_POWER, 30000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 30000) - .output(LEVL_PUC_BATTERY_POWER, 20000L) + .output(PUC_BATTERY_POWER, 20000L) .output(LEVL_REMAINING_LEVL_ENERGY, 0L) .output(LEVL_SOC, 87500L)) .next(new TestCase() @@ -149,7 +149,7 @@ public void testWithLevlDischargeRequest() throws Exception { .input(METER_ACTIVE_POWER, -10000) .input(DEBUG_SET_ACTIVE_POWER, 20000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 20000) - .output(LEVL_PUC_BATTERY_POWER, 20000L) + .output(PUC_BATTERY_POWER, 20000L) .output(LEVL_REMAINING_LEVL_ENERGY, 0L) .output(LEVL_SOC, 87500L)) .next(new TestCase() @@ -157,7 +157,7 @@ public void testWithLevlDischargeRequest() throws Exception { .input(METER_ACTIVE_POWER, 0) .input(DEBUG_SET_ACTIVE_POWER, 20000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 20000) - .output(LEVL_PUC_BATTERY_POWER, 20000L) + .output(PUC_BATTERY_POWER, 20000L) .output(LEVL_REMAINING_LEVL_ENERGY, 0L) .output(LEVL_SOC, 87500L)); } @@ -194,7 +194,7 @@ public void testWithLevlChargeRequest() throws Exception { .input(METER_ACTIVE_POWER, 20000) .input(DEBUG_SET_ACTIVE_POWER, 10000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 10000) - .output(LEVL_PUC_BATTERY_POWER, 20000L) + .output(PUC_BATTERY_POWER, 20000L) .output(LEVL_REMAINING_LEVL_ENERGY, 0L) .output(LEVL_SOC, 108000L)) .next(new TestCase() @@ -202,7 +202,7 @@ public void testWithLevlChargeRequest() throws Exception { .input(METER_ACTIVE_POWER, 10000) .input(DEBUG_SET_ACTIVE_POWER, 20000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 20000) - .output(LEVL_PUC_BATTERY_POWER, 20000L) + .output(PUC_BATTERY_POWER, 20000L) .output(LEVL_REMAINING_LEVL_ENERGY, 0L) .output(LEVL_SOC, 108000L)) .next(new TestCase() @@ -210,7 +210,7 @@ public void testWithLevlChargeRequest() throws Exception { .input(METER_ACTIVE_POWER, 0) .input(DEBUG_SET_ACTIVE_POWER, 20000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 20000) - .output(LEVL_PUC_BATTERY_POWER, 20000L) + .output(PUC_BATTERY_POWER, 20000L) .output(LEVL_REMAINING_LEVL_ENERGY, 0L) .output(LEVL_SOC, 108000L)); } @@ -248,7 +248,7 @@ public void testWithLargeLevlDischargeRequest() throws Exception { .input(METER_ACTIVE_POWER, 20000) .input(DEBUG_SET_ACTIVE_POWER, 120000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 120000) - .output(LEVL_PUC_BATTERY_POWER, 20000L) + .output(PUC_BATTERY_POWER, 20000L) .output(LEVL_REMAINING_LEVL_ENERGY, 2_499_900_000L) .output(LEVL_SOC, -25000L)) .next(new TestCase() @@ -256,7 +256,7 @@ public void testWithLargeLevlDischargeRequest() throws Exception { .input(METER_ACTIVE_POWER, -100000) .input(DEBUG_SET_ACTIVE_POWER, 120000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 120000) - .output(LEVL_PUC_BATTERY_POWER, 20000L) + .output(PUC_BATTERY_POWER, 20000L) .output(LEVL_REMAINING_LEVL_ENERGY, 2_499_800_000L) .output(LEVL_SOC, -150000L)) .next(new TestCase() @@ -264,7 +264,7 @@ public void testWithLargeLevlDischargeRequest() throws Exception { .input(METER_ACTIVE_POWER, -100000) .input(DEBUG_SET_ACTIVE_POWER, 120000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 120000) - .output(LEVL_PUC_BATTERY_POWER, 20000L) + .output(PUC_BATTERY_POWER, 20000L) .output(LEVL_REMAINING_LEVL_ENERGY, 2_499_700_000L) .output(LEVL_SOC, -275000L)); } @@ -301,7 +301,7 @@ public void testWithReservedChargeCapacityLevlChargesPucMustNotCharge() throws E .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) - .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl + .output(PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl .output(LEVL_REMAINING_LEVL_ENERGY, -9_500_000L) // 500,000 Ws should be realized, therefore 9,500,000 Ws are remaining .output(LEVL_SOC, -179_600_000L)) // Levl soc increases by 500,000 Ws * 80% efficiency = 400,000 Ws .next(new TestCase() @@ -310,7 +310,7 @@ public void testWithReservedChargeCapacityLevlChargesPucMustNotCharge() throws E .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) - .output(LEVL_PUC_BATTERY_POWER, -20_000L) // 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) // since reserved capacity decreased by 400,000 Ws in the previous cycle but ess soc value remains the same, puc can charge again .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // 480,000 Ws can be realized for Levl, therefore 9,020,00 are remaining .output(LEVL_SOC, -179_216_000L)); // Levl soc increases by 480,000 Ws * 80% efficiency = 384,000 Ws } @@ -347,7 +347,7 @@ public void testWithReservedChargeCapacityLevlDischargesPucMustNotCharge() throw .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) - .output(LEVL_PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl + .output(PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl .output(LEVL_REMAINING_LEVL_ENERGY, 9_500_000L) // 500,000 Ws should be realized, therefore 9,500,000 Ws are remaining .output(LEVL_SOC, -180_625_000L)) // Levl soc decreases by 500,000 Ws / 80% efficiency = 625,000 Ws .next(new TestCase() @@ -356,7 +356,7 @@ public void testWithReservedChargeCapacityLevlDischargesPucMustNotCharge() throw .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) - .output(LEVL_PUC_BATTERY_POWER, 0L) // // puc should not do anything because capacity is still completely reserved for Levl + .output(PUC_BATTERY_POWER, 0L) // // puc should not do anything because capacity is still completely reserved for Levl .output(LEVL_REMAINING_LEVL_ENERGY, 9_000_000L) // 500,000 Ws should be realized, therefore 9,000,000 Ws are remaining .output(LEVL_SOC, -181_250_000L)); // Levl soc decreases by 500,000 Ws / 80% efficiency = 625,000 Ws } @@ -394,7 +394,7 @@ public void testWithReservedChargeCapacityLevlChargesPucMayCharge() throws Excep .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) - .output(LEVL_PUC_BATTERY_POWER, -10_000L) // puc should charge 10,000 Ws since 5% capacity is available + .output(PUC_BATTERY_POWER, -10_000L) // puc should charge 10,000 Ws since 5% capacity is available .output(LEVL_REMAINING_LEVL_ENERGY, -9_510_000L) // 490,000 Ws should be realized, therefore 9,510,000 Ws are remaining .output(LEVL_SOC, -179_608_000L)) // Levl soc increases by 490,000 Ws * 80% efficiency = 392,000 Ws .next(new TestCase() @@ -403,7 +403,7 @@ public void testWithReservedChargeCapacityLevlChargesPucMayCharge() throws Excep .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(LEVL_PUC_BATTERY_POWER, -10_000L) // puc should still charge 10,000 Ws + .output(PUC_BATTERY_POWER, -10_000L) // puc should still charge 10,000 Ws .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // 490,000 Ws should be realized, therefore 9,020,000 Ws are remaining .output(LEVL_SOC, -179_216_000L)); // Levl soc increases by 490,000 Ws * 80% efficiency = 392,000 Ws } @@ -440,7 +440,7 @@ public void testWithReservedChargeCapacityLevlChargesPucMayDischarge() throws Ex .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) - .output(LEVL_PUC_BATTERY_POWER, 10_000L) // puc should discharge 10,000 Ws since Levl has reserved charge not discharge energy + .output(PUC_BATTERY_POWER, 10_000L) // puc should discharge 10,000 Ws since Levl has reserved charge not discharge energy .output(LEVL_REMAINING_LEVL_ENERGY, -9_490_000L) // 510,000 Ws can be realized, because puc discharges 10,000 Ws .output(LEVL_SOC, -179_592_000L)) // Levl soc increases by 510,000 Ws * 80% efficiency = 408,000 Ws .next(new TestCase() @@ -449,7 +449,7 @@ public void testWithReservedChargeCapacityLevlChargesPucMayDischarge() throws Ex .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(LEVL_PUC_BATTERY_POWER, 10_000L) // puc should still charge 10,000 Ws + .output(PUC_BATTERY_POWER, 10_000L) // puc should still charge 10,000 Ws .output(LEVL_REMAINING_LEVL_ENERGY, -8_980_000L) // 510,000 Ws can be realized again, therefore 8,980,000 Ws are remaining .output(LEVL_SOC, -179_184_000L)); // Levl soc increases by 510,000 Ws * 80% efficiency = 408,000 Ws } @@ -485,7 +485,7 @@ public void testInfluenceSellToGrid_PucSellToGrid_LevlChargeForbidden() throws E .input(METER_ACTIVE_POWER, -20000) .input(DEBUG_SET_ACTIVE_POWER, 0) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) - .output(LEVL_PUC_BATTERY_POWER, 0L) + .output(PUC_BATTERY_POWER, 0L) .output(LEVL_REMAINING_LEVL_ENERGY, -10000L) .output(LEVL_SOC, -180_000_000L)); } @@ -520,7 +520,7 @@ public void testInfluenceSellToGrid_PucSellToGrid_LevlDischargeForbidden() throw .input(METER_ACTIVE_POWER, -20000) .input(DEBUG_SET_ACTIVE_POWER, 0) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) - .output(LEVL_PUC_BATTERY_POWER, 0L) + .output(PUC_BATTERY_POWER, 0L) .output(LEVL_REMAINING_LEVL_ENERGY, 10000L) .output(LEVL_SOC, -180_000_000L)); } @@ -555,7 +555,7 @@ public void testInfluenceSellToGrid_PucBuyFromGrid_LevlChargeAllowed() throws Ex .input(METER_ACTIVE_POWER, 20000) .input(DEBUG_SET_ACTIVE_POWER, -30000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -30000) - .output(LEVL_PUC_BATTERY_POWER, 0L) + .output(PUC_BATTERY_POWER, 0L) .output(LEVL_REMAINING_LEVL_ENERGY, 0L) .output(LEVL_SOC, 24000L)); } @@ -590,7 +590,7 @@ public void testInfluenceSellToGrid_PucBuyFromGrid_LevlDischargeLimited() throws .input(METER_ACTIVE_POWER, 20000) .input(DEBUG_SET_ACTIVE_POWER, 20000) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 20000) - .output(LEVL_PUC_BATTERY_POWER, 0L) + .output(PUC_BATTERY_POWER, 0L) .output(LEVL_REMAINING_LEVL_ENERGY, 10000L) .output(LEVL_SOC, 179_980_000L)); } @@ -627,7 +627,7 @@ public void testUpperSocLimit() throws Exception { .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(LEVL_PUC_BATTERY_POWER, -0L) + .output(PUC_BATTERY_POWER, -0L) .output(LEVL_REMAINING_LEVL_ENERGY, -82_000_000L) .output(LEVL_SOC, 18_000_000L)) .next(new TestCase() @@ -636,7 +636,7 @@ public void testUpperSocLimit() throws Exception { .input(METER_ACTIVE_POWER, 18_000_000) .input(DEBUG_SET_ACTIVE_POWER, 0) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) - .output(LEVL_PUC_BATTERY_POWER, 0L) + .output(PUC_BATTERY_POWER, 0L) .output(LEVL_REMAINING_LEVL_ENERGY, -82_000_000L) .output(LEVL_SOC, 18_000_000L)) .next(new TestCase() @@ -645,10 +645,83 @@ public void testUpperSocLimit() throws Exception { .input(METER_ACTIVE_POWER, 0) .input(DEBUG_SET_ACTIVE_POWER, 0) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) - .output(LEVL_PUC_BATTERY_POWER, 0L) + .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 { + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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)) + .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 { @@ -682,7 +755,7 @@ public void testLowerSocLimit() throws Exception { .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(LEVL_PUC_BATTERY_POWER, -0L) + .output(PUC_BATTERY_POWER, -0L) .output(LEVL_REMAINING_LEVL_ENERGY, 82_000_000L) .output(LEVL_SOC, -18_000_000L)) .next(new TestCase() @@ -691,7 +764,7 @@ public void testLowerSocLimit() throws Exception { .input(METER_ACTIVE_POWER, -18_000_000) .input(DEBUG_SET_ACTIVE_POWER, 0) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) - .output(LEVL_PUC_BATTERY_POWER, 0L) + .output(PUC_BATTERY_POWER, 0L) .output(LEVL_REMAINING_LEVL_ENERGY, 82_000_000L) .output(LEVL_SOC, -18_000_000L)) .next(new TestCase() @@ -700,8 +773,81 @@ public void testLowerSocLimit() throws Exception { .input(METER_ACTIVE_POWER, 0) .input(DEBUG_SET_ACTIVE_POWER, 0) .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 0) - .output(LEVL_PUC_BATTERY_POWER, 0L) + .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 { + new ControllerTest(new ControllerEssBalancingImpl()) + .addReference("cm", new DummyConfigurationAdmin()) + .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)) + .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)); + } } From a0acffecd4203bf0f8f003436a8075faf2ba20d0 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Wed, 13 Nov 2024 14:12:45 +0100 Subject: [PATCH 32/41] levl-1290/1302: change bundle properties --- io.openems.edge.levl.controller/bnd.bnd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/io.openems.edge.levl.controller/bnd.bnd b/io.openems.edge.levl.controller/bnd.bnd index 22e747a769f..9fc8bcc756b 100644 --- a/io.openems.edge.levl.controller/bnd.bnd +++ b/io.openems.edge.levl.controller/bnd.bnd @@ -1,7 +1,7 @@ -Bundle-Name: OpenEMS Edge Controller Ess Balancing -Bundle-Vendor: FENECON GmbH +Bundle-Name: Ess Balancing with flexibility trading +Bundle-Vendor: Levl Energy GmbH Bundle-License: https://opensource.org/licenses/EPL-2.0 -Bundle-Version: 1.0.0.${tstamp} +Bundle-Version: 0.1.0.${tstamp} -buildpath: \ ${buildpath},\ From eb591b2b08341871bf1091004df011c32b3f24ed Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Wed, 13 Nov 2024 14:42:55 +0100 Subject: [PATCH 33/41] levl-1290/1302: move edgeapp config lines to the right place --- io.openems.edge.application/EdgeApp.bndrun | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index 1857921a1d2..46a0112b70d 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -145,6 +145,7 @@ bnd.identity;id='io.openems.edge.kaco.blueplanet.hybrid10',\ bnd.identity;id='io.openems.edge.katek.edcom',\ bnd.identity;id='io.openems.edge.kostal.piko',\ + bnd.identity;id='io.openems.edge.levl.controller',\ bnd.identity;id='io.openems.edge.meter.abb',\ bnd.identity;id='io.openems.edge.meter.artemes.am2',\ bnd.identity;id='io.openems.edge.meter.bcontrol.em300',\ @@ -192,7 +193,6 @@ bnd.identity;id='io.openems.edge.timeofusetariff.rabotcharge',\ bnd.identity;id='io.openems.edge.timeofusetariff.swisspower',\ bnd.identity;id='io.openems.edge.timeofusetariff.tibber',\ - bnd.identity;id='io.openems.edge.levl.controller',\ -runbundles: \ Java-WebSocket;version='[1.5.4,1.5.5)',\ @@ -323,6 +323,7 @@ io.openems.edge.kaco.blueplanet.hybrid10;version=snapshot,\ io.openems.edge.katek.edcom;version=snapshot,\ io.openems.edge.kostal.piko;version=snapshot,\ + io.openems.edge.levl.controller;version=snapshot,\ io.openems.edge.meter.abb;version=snapshot,\ io.openems.edge.meter.api;version=snapshot,\ io.openems.edge.meter.artemes.am2;version=snapshot,\ @@ -434,6 +435,5 @@ org.owasp.encoder;version='[1.3.1,1.3.2)',\ reactive-streams;version='[1.0.4,1.0.5)',\ rrd4j;version='[3.9.0,3.9.1)',\ - stax2-api;version='[4.2.2,4.2.3)',\ - io.openems.edge.levl.controller;version=snapshot + stax2-api;version='[4.2.2,4.2.3)' \ No newline at end of file From 020dad493e7bb69540afc2d174e137563a9c186f Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Wed, 13 Nov 2024 15:41:59 +0100 Subject: [PATCH 34/41] levl-1290/1302: clean up and sort bnd files --- io.openems.edge.application/EdgeApp.bndrun | 3 +-- io.openems.edge.levl.controller/bnd.bnd | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index 46a0112b70d..1e4a645314b 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -435,5 +435,4 @@ org.owasp.encoder;version='[1.3.1,1.3.2)',\ reactive-streams;version='[1.0.4,1.0.5)',\ rrd4j;version='[3.9.0,3.9.1)',\ - stax2-api;version='[4.2.2,4.2.3)' - \ No newline at end of file + stax2-api;version='[4.2.2,4.2.3)' \ No newline at end of file diff --git a/io.openems.edge.levl.controller/bnd.bnd b/io.openems.edge.levl.controller/bnd.bnd index 9fc8bcc756b..83f7ea62672 100644 --- a/io.openems.edge.levl.controller/bnd.bnd +++ b/io.openems.edge.levl.controller/bnd.bnd @@ -9,8 +9,8 @@ Bundle-Version: 0.1.0.${tstamp} io.openems.edge.common,\ io.openems.edge.controller.api,\ io.openems.edge.ess.api,\ - io.openems.edge.meter.api,\ - io.openems.edge.ess.generic + io.openems.edge.ess.generic,\ + io.openems.edge.meter.api -testpath: \ ${testpath} From aa4f5ee123669f63e89be87911db152f0251e784 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Fri, 15 Nov 2024 16:29:32 +0100 Subject: [PATCH 35/41] levl-1329/1332: implement pull request review comments --- .../controller/ControllerEssBalancing.java | 113 +- .../ControllerEssBalancingImpl.java | 96 +- .../levl/controller/LevlControlRequest.java | 28 +- .../levl/controller/common/Efficiency.java | 48 +- .../levl/controller/BalancingImplTest.java | 1441 +++++++++-------- .../ControllerEssBalancingImplTest.java | 4 +- 6 files changed, 902 insertions(+), 828 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java index 53146507036..ca595d7fdc2 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -15,34 +15,55 @@ public interface ControllerEssBalancing extends Controller, OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - REMAINING_LEVL_ENERGY(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH) - .text("energy to be realized [Ws]")), - LEVL_SOC(Doc.of(OpenemsType.LONG).unit(Unit.WATT_HOURS).persistencePriority(PersistencePriority.HIGH) - .text("levl state of charge [Wh]")), - SELL_TO_GRID_LIMIT(Doc.of(OpenemsType.LONG).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH) - .text("maximum power that may be sold to the grid [W]")), - BUY_FROM_GRID_LIMIT(Doc.of(OpenemsType.LONG).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH) - .text("maximum power that may be bought from the grid [W]")), - SOC_LOWER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH) - .text("lower soc bound levl has to respect [%]")), - SOC_UPPER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH) - .text("upper soc bound levl has to respect [%]")), - INFLUENCE_SELL_TO_GRID(Doc.of(OpenemsType.BOOLEAN).persistencePriority(PersistencePriority.HIGH) - .text("defines if levl is allowed to influence the sell to grid power [true/false]")), - EFFICIENCY(Doc.of(OpenemsType.DOUBLE).unit(Unit.PERCENT).persistencePriority(PersistencePriority.HIGH) - .text("ess efficiency defined by levl [%]")), - PUC_BATTERY_POWER(Doc.of(OpenemsType.LONG).unit(Unit.WATT).persistencePriority(PersistencePriority.HIGH) - .text("power that is applied for the ess primary use case")), - REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH) - .text("energy realized for the current request on the grid [Ws])")), - REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH) - .text("energy realized for the current request in the battery [Ws])")), - LAST_REQUEST_REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH).text( - "energy that has been realized for the last request on the grid [Ws]")), - LAST_REQUEST_REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG).persistencePriority(PersistencePriority.HIGH) - .text("energy that has been realized for the last request in the battery [Ws]")), - LAST_REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING).persistencePriority(PersistencePriority.HIGH) - .text("the timestamp of the last levl control request")); + REMAINING_LEVL_ENERGY(Doc.of(OpenemsType.LONG) // + .persistencePriority(PersistencePriority.HIGH) // + .text("energy to be realized [Ws]")), // + LEVL_STATE_OF_CHARGE(Doc.of(OpenemsType.LONG) // + .unit(Unit.WATT_HOURS) // + .persistencePriority(PersistencePriority.HIGH) // + .text("levl state of charge [Wh]")), // + SELL_TO_GRID_LIMIT(Doc.of(OpenemsType.LONG) // + .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.HIGH) // + .text("maximum power that may be sold to the grid [W]")), // + BUY_FROM_GRID_LIMIT(Doc.of(OpenemsType.LONG) // + .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.HIGH) // + .text("maximum power that may be bought from the grid [W]")), // + STATE_OF_CHARGE_LOWER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE) // + .unit(Unit.PERCENT) // + .persistencePriority(PersistencePriority.HIGH) // + .text("lower soc bound levl has to respect [%]")), // + STATE_OF_CHARGE_UPPER_BOUND_LEVL(Doc.of(OpenemsType.DOUBLE) // + .unit(Unit.PERCENT) // + .persistencePriority(PersistencePriority.HIGH) // + .text("upper soc bound levl has to respect [%]")), // + INFLUENCE_SELL_TO_GRID(Doc.of(OpenemsType.BOOLEAN) // + .persistencePriority(PersistencePriority.HIGH) // + .text("defines if levl is allowed to influence the sell to grid power [true/false]")), // + ESS_EFFICIENCY(Doc.of(OpenemsType.DOUBLE) // + .unit(Unit.PERCENT) // + .persistencePriority(PersistencePriority.HIGH) // + .text("ess efficiency defined by levl [%]")), // + PRIMARY_USE_CASE_BATTERY_POWER(Doc.of(OpenemsType.LONG) // + .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.HIGH) // + .text("power that is applied for the ess primary use case")), // + REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG) // + .persistencePriority(PersistencePriority.HIGH) // + .text("energy realized for the current request on the grid [Ws])")), // + REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG) // + .persistencePriority(PersistencePriority.HIGH) // + .text("energy realized for the current request in the battery [Ws])")), // + LAST_REQUEST_REALIZED_ENERGY_GRID(Doc.of(OpenemsType.LONG) // + .persistencePriority(PersistencePriority.HIGH) // + .text("energy that has been realized for the last request on the grid [Ws]")), // + LAST_REQUEST_REALIZED_ENERGY_BATTERY(Doc.of(OpenemsType.LONG) // + .persistencePriority(PersistencePriority.HIGH) // + .text("energy that has been realized for the last request in the battery [Ws]")), // + LAST_REQUEST_TIMESTAMP(Doc.of(OpenemsType.STRING) // + .persistencePriority(PersistencePriority.HIGH) // + .text("the timestamp of the last levl control request")); // private final Doc doc; @@ -89,7 +110,7 @@ public default void _setRemainingLevlEnergy(Long value) { * @return the LongReadChannel */ public default LongReadChannel getLevlSocChannel() { - return this.channel(ChannelId.LEVL_SOC); + return this.channel(ChannelId.LEVL_STATE_OF_CHARGE); } /** @@ -170,7 +191,7 @@ public default void _setBuyFromGridLimit(Long value) { * @return the DoubleReadChannel */ public default DoubleReadChannel getSocLowerBoundLevlChannel() { - return this.channel(ChannelId.SOC_LOWER_BOUND_LEVL); + return this.channel(ChannelId.STATE_OF_CHARGE_LOWER_BOUND_LEVL); } /** @@ -197,7 +218,7 @@ public default void _setSocLowerBoundLevl(Double value) { * @return the DoubleReadChannel */ public default DoubleReadChannel getSocUpperBoundLevlChannel() { - return this.channel(ChannelId.SOC_UPPER_BOUND_LEVL); + return this.channel(ChannelId.STATE_OF_CHARGE_UPPER_BOUND_LEVL); } /** @@ -246,52 +267,52 @@ public default void _setInfluenceSellToGrid(Boolean value) { } /** - * Returns the DoubleReadChannel for the efficiency. + * Returns the DoubleReadChannel for the ess efficiency. * * @return the DoubleReadChannel */ - public default DoubleReadChannel getEfficiencyChannel() { - return this.channel(ChannelId.EFFICIENCY); + public default DoubleReadChannel getEssEfficiencyChannel() { + return this.channel(ChannelId.ESS_EFFICIENCY); } /** - * Returns the value of the efficiency. + * Returns the value of the ess efficiency. * - * @return the value of the efficiency + * @return the value of the ess efficiency */ - public default Value getEfficiency() { - return this.getEfficiencyChannel().value(); + public default Value getEssEfficiency() { + return this.getEssEfficiencyChannel().value(); } /** - * Sets the next value of the efficiency. + * Sets the next value of the ess efficiency. * * @param value the next value */ - public default void _setEfficiency(Double value) { - this.getEfficiencyChannel().setNextValue(value); + public default void _setEssEfficiency(Double value) { + this.getEssEfficiencyChannel().setNextValue(value); } /** - * Returns the LongReadChannel for the PUC battery power. + * Returns the LongReadChannel for the puc (primary use case) battery power. * * @return the LongReadChannel */ public default LongReadChannel getPucBatteryPowerChannel() { - return this.channel(ChannelId.PUC_BATTERY_POWER); + return this.channel(ChannelId.PRIMARY_USE_CASE_BATTERY_POWER); } /** - * Returns the value of the PUC battery power. + * Returns the value of the puc (primary use case) battery power. * - * @return the value of the PUC battery power + * @return the value of the puc battery power */ public default Value getPucBatteryPower() { return this.getPucBatteryPowerChannel().value(); } /** - * Sets the next value of the PUC battery power. + * Sets the next value of the puc (primary use case) battery power. * * @param value the next value */ diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 4a17ad188cc..f12c19dbde8 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -38,6 +38,10 @@ import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.levl.controller.common.Efficiency; +import static java.lang.Math.round; +import static java.lang.Math.min; +import static java.lang.Math.max; + @Designate(ocd = Config.class, factory = true) @Component(name = "Controller.Levl.Symmetric.Balancing", immediate = true, configurationPolicy = ConfigurationPolicy.REQUIRE) @EventTopics({ EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, }) @@ -45,6 +49,13 @@ public class ControllerEssBalancingImpl extends AbstractOpenemsComponent implements Controller, OpenemsComponent, ControllerEssBalancing, ComponentJsonApi, EventHandler { + private static final double HUNDRED_PERCENT = 100.0; + private static final long SECONDS_PER_HOUR = 3600L; + private static final double MILLISECONDS_PER_SECOND = 1000.0; + private static final String METHOD = "sendLevlControlRequest"; + + protected static Clock clock = Clock.systemDefaultZone(); + private final Logger log = LoggerFactory.getLogger(ControllerEssBalancingImpl.class); @Reference @@ -59,13 +70,9 @@ public class ControllerEssBalancingImpl extends AbstractOpenemsComponent @Reference protected Cycle cycle; - protected static Clock clock = Clock.systemDefaultZone(); - protected LevlControlRequest currentRequest; protected LevlControlRequest nextRequest; - private static final String METHOD = "sendLevlControlRequest"; - public ControllerEssBalancingImpl() { super(// OpenemsComponent.ChannelId.values(), // @@ -130,7 +137,7 @@ private void initChannelValues() { this._setLastRequestTimestamp("1970-01-02T00:00:00Z"); this._setLevlSoc(0L); this._setPucBatteryPower(0L); - this._setEfficiency(100.0); + this._setEssEfficiency(100.0); this._setSocLowerBoundLevl(0.0); this._setSocUpperBoundLevl(0.0); this._setRemainingLevlEnergy(0L); @@ -147,7 +154,7 @@ private void initChannelValues() { * @throws OpenemsNamedException on error */ protected int calculateRequiredPower() throws OpenemsNamedException { - var cycleTimeS = this.cycle.getCycleTime() / 1000.0; + var cycleTimeS = this.cycle.getCycleTime() / MILLISECONDS_PER_SECOND; // load physical values var physicalSoc = this.ess.getSoc().getOrError(); @@ -160,22 +167,22 @@ protected int calculateRequiredPower() throws OpenemsNamedException { // levl request specific values var levlSocWs = this.getLevlSoc().getOrError(); var remainingLevlEnergyWs = this.getRemainingLevlEnergy().getOrError(); - var efficiency = this.getEfficiency().getOrError(); + var efficiency = this.getEssEfficiency().getOrError(); var socLowerBoundLevlPercent = this.getSocLowerBoundLevl().getOrError(); var socUpperBoundLevlPercent = this.getSocUpperBoundLevl().getOrError(); var buyFromGridLimit = this.getBuyFromGridLimit().getOrError(); var sellToGridLimit = this.getSellToGridLimit().getOrError(); var influenceSellToGrid = this.getInfluenceSellToGrid().getOrError(); - var essCapacityWs = essCapacity * 3600L; - var physicalSocWs = Math.round((physicalSoc / 100.0) * essCapacityWs); + var essCapacityWs = essCapacity * SECONDS_PER_HOUR; + var physicalSocWs = round((physicalSoc / HUNDRED_PERCENT) * essCapacityWs); // primary use case (puc) calculation var pucSocWs = this.calculatePucSoc(physicalSocWs, levlSocWs, essCapacityWs); var pucBatteryPower = this.calculatePucBatteryPower(gridPower, essPower, pucSocWs, essCapacityWs, minEssPower, maxEssPower, efficiency, cycleTimeS); var pucGridPower = gridPower + essPower - pucBatteryPower; - var nextPucSocWs = pucSocWs - Math.round(Efficiency.apply(pucBatteryPower, efficiency) * cycleTimeS); + var nextPucSocWs = pucSocWs - round(Efficiency.apply(pucBatteryPower, efficiency) * cycleTimeS); // levl calculation var levlBatteryPower = 0; @@ -218,8 +225,7 @@ protected long calculatePucSoc(long physicalSocWs, long levlSocWs, long essCapac * Calculates the power of the primary use case, taking into account the ess * power limits and the soc limits. *

- * positive = discharge - * negative = charge + * positive = discharge; negative = charge *

* * @param gridPower the active power of the meter [W] @@ -245,7 +251,7 @@ protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocW this.log.debug("pucBatteryPower without limits: " + pucBatteryPower); // apply ess power limits - pucBatteryPower = Math.max(Math.min(pucBatteryPower, maxEssPower), minEssPower); + pucBatteryPower = max(min(pucBatteryPower, maxEssPower), minEssPower); this.log.debug("pucBatteryPower with ess power limits: " + pucBatteryPower); // apply soc bounds @@ -267,11 +273,12 @@ protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocW */ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, double efficiency, double cycleTimeS) { + // efficiency = 50 var dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; var dischargeEnergyUpperBoundWs = pucSocWs; - var powerLowerBound = Efficiency.unapply(Math.round(dischargeEnergyLowerBoundWs / cycleTimeS), efficiency); - var powerUpperBound = Efficiency.unapply(Math.round(dischargeEnergyUpperBoundWs / cycleTimeS), efficiency); + var powerLowerBound = Efficiency.unapply(round(dischargeEnergyLowerBoundWs / cycleTimeS), efficiency); + var powerUpperBound = Efficiency.unapply(round(dischargeEnergyUpperBoundWs / cycleTimeS), efficiency); if (powerLowerBound > 0) { powerLowerBound = 0; @@ -280,15 +287,14 @@ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, powerUpperBound = 0; } - return (int) Math.max(Math.min(pucPower, powerUpperBound), powerLowerBound); + return (int) applyBound(pucPower, powerLowerBound, powerUpperBound); } /** * Calculates the battery power for the levl use case considering various * constraints. *

- * positive = discharge - * negative = charge + * positive = discharge; negative = charge *

* * @param remainingLevlEnergyWs the remaining energy that has to be realized @@ -319,15 +325,16 @@ protected int calculateLevlBatteryPower(long remainingLevlEnergyWs, int pucBatte 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 = Math.round(remainingLevlEnergyWs / (double) cycleTimeS); + 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); @@ -359,14 +366,15 @@ protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBattery int maxEssPower) { var levlPowerLowerBound = Long.valueOf(minEssPower) - pucBatteryPower; var levlPowerUpperBound = Long.valueOf(maxEssPower) - pucBatteryPower; - return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); + return applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); } /** * 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 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 [%] @@ -378,8 +386,8 @@ protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBattery protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long levlSocWs, double socLowerBoundLevlPercent, double socUpperBoundLevlPercent, long essCapacityWs, double efficiency, double cycleTimeS) { - var levlSocLowerBoundWs = Math.round(socLowerBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; - var levlSocUpperBoundWs = Math.round(socUpperBoundLevlPercent / 100.0 * essCapacityWs) - nextPucSocWs; + var levlSocLowerBoundWs = round(socLowerBoundLevlPercent / HUNDRED_PERCENT * essCapacityWs) - nextPucSocWs; + var levlSocUpperBoundWs = round(socUpperBoundLevlPercent / HUNDRED_PERCENT * essCapacityWs) - nextPucSocWs; if (levlSocLowerBoundWs > 0) { levlSocLowerBoundWs = 0; @@ -391,12 +399,10 @@ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, var levlDischargeEnergyLowerBoundWs = -(levlSocUpperBoundWs - levlSocWs); var levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - levlSocWs); - var levlPowerLowerBound = Efficiency.unapply(Math.round(levlDischargeEnergyLowerBoundWs / cycleTimeS), - efficiency); - var levlPowerUpperBound = Efficiency.unapply(Math.round(levlDischargeEnergyUpperBoundWs / cycleTimeS), - efficiency); + var levlPowerLowerBound = Efficiency.unapply(round(levlDischargeEnergyLowerBoundWs / cycleTimeS), efficiency); + var levlPowerUpperBound = Efficiency.unapply(round(levlDischargeEnergyUpperBoundWs / cycleTimeS), efficiency); - return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); + return applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); } /** @@ -412,7 +418,7 @@ protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, long sellToGridLimit) { var levlPowerLowerBound = -(buyFromGridLimit - pucGridPower); var levlPowerUpperBound = -(sellToGridLimit - pucGridPower); - return Math.max(Math.min(levlPower, levlPowerUpperBound), levlPowerLowerBound); + return applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); } /** @@ -430,7 +436,7 @@ protected long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPow levlPower = 0; } else { // if primary use case buys from grid, levl can sell maximum this amount to grid - levlPower = Math.min(levlPower, pucGridPower); + levlPower = min(levlPower, pucGridPower); } } return levlPower; @@ -456,7 +462,7 @@ protected JsonrpcResponse handleRequest(Call ca this.log.info("Received new levl request: {}", request); this.nextRequest = request; var realizedEnergyBatteryWs = this.getRealizedEnergyBattery().getOrError(); - var updatedLevlSoc = request.levlSocWh * 3600L - realizedEnergyBatteryWs; + var updatedLevlSoc = request.levlSocWh * SECONDS_PER_HOUR - realizedEnergyBatteryWs; this._setLevlSoc(updatedLevlSoc); this.log.info("Updated levl soc: {}", updatedLevlSoc); return JsonrpcResponseSuccess.from(this.generateResponse(call.getRequest().getId(), request.levlRequestId)); @@ -507,7 +513,7 @@ public void handleEvent(Event event) { */ private void handleAfterWriteEvent() throws OpenemsNamedException { var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); - + if (remainingLevlEnergy != 0L) { var pucBatteryPower = this.getPucBatteryPowerChannel().getNextValue().getOrError(); var levlPower = 0L; @@ -517,7 +523,7 @@ private void handleAfterWriteEvent() throws OpenemsNamedException { levlPower = essPower - pucBatteryPower; } - var levlEnergyWs = Math.round(levlPower * (this.cycle.getCycleTime() / 1000.0)); + var levlEnergyWs = round(levlPower * (this.cycle.getCycleTime() / MILLISECONDS_PER_SECOND)); // remaining for the next cycle var newRemainingLevlEnergy = remainingLevlEnergy - levlEnergyWs; @@ -531,7 +537,7 @@ private void handleAfterWriteEvent() throws OpenemsNamedException { this._setRealizedEnergyGrid(realizedEnergyGridWs + levlEnergyWs); // realized after the next cycle - var efficiency = this.getEfficiency().getOrError(); + var efficiency = this.getEssEfficiency().getOrError(); var realizedEnergyBatteryWs = this.getRealizedEnergyBattery().getOrError(); this._setRealizedEnergyBattery(realizedEnergyBatteryWs + Efficiency.apply(levlEnergyWs, efficiency)); @@ -568,7 +574,7 @@ private void startNextRequest() { this.log.info("starting levl request: {}", this.nextRequest); this.currentRequest = this.nextRequest; this.nextRequest = null; - this._setEfficiency(this.currentRequest.efficiencyPercent); + this._setEssEfficiency(this.currentRequest.efficiencyPercent); this._setSocLowerBoundLevl(this.currentRequest.socLowerBoundPercent); this._setSocUpperBoundLevl(this.currentRequest.socUpperBoundPercent); this._setRemainingLevlEnergy(this.currentRequest.energyWs); @@ -580,4 +586,8 @@ private void startNextRequest() { private boolean hasSignChanged(long a, long b) { return a < 0 && b > 0 || a > 0 && b < 0; } + + private long applyBound(long power, long lowerBound, long upperBound) { + return max(min(power, upperBound), lowerBound); + } } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index 7d12c18fa47..b8a1b4fce1c 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -4,6 +4,7 @@ import io.openems.common.exceptions.OpenemsError; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.utils.JsonUtils; import java.time.Clock; import java.time.LocalDateTime; @@ -30,7 +31,6 @@ public LevlControlRequest() { } - public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedException { try { this.parseFields(params); @@ -83,20 +83,20 @@ public static LevlControlRequest from(JsonrpcRequest request) throws OpenemsName return new LevlControlRequest(params); } - private void parseFields(JsonObject params) { - this.levlRequestId = params.get("levlRequestId").getAsString(); - this.timestamp = params.get("levlRequestTimestamp").getAsString(); - this.energyWs = params.get("levlPowerW").getAsLong() * QUARTER_HOUR_SECONDS; + private void parseFields(JsonObject params) throws OpenemsNamedException { + this.levlRequestId = JsonUtils.getAsString(params, "levlRequestId"); + this.timestamp = JsonUtils.getAsString(params, "levlRequestTimestamp"); + this.energyWs = JsonUtils.getAsLong(params, "levlPowerW") * QUARTER_HOUR_SECONDS; this.start = LocalDateTime.now(LevlControlRequest.clock) - .plusSeconds(params.get("levlChargeDelaySec").getAsInt()); - this.deadline = this.start.plusSeconds(params.get("levlChargeDurationSec").getAsInt()); - this.levlSocWh = params.get("levlSocWh").getAsInt(); - this.socLowerBoundPercent = params.get("levlSocLowerBoundPercent").getAsDouble(); - this.socUpperBoundPercent = params.get("levlSocUpperBoundPercent").getAsDouble(); - this.sellToGridLimitW = params.get("sellToGridLimitW").getAsInt(); - this.buyFromGridLimitW = params.get("buyFromGridLimitW").getAsInt(); - this.efficiencyPercent = params.get("efficiencyPercent").getAsDouble(); - this.influenceSellToGrid = params.get("influenceSellToGrid").getAsBoolean(); + .plusSeconds(JsonUtils.getAsInt(params, "levlChargeDelaySec")); + this.deadline = this.start.plusSeconds(JsonUtils.getAsInt(params, "levlChargeDurationSec")); + this.levlSocWh = JsonUtils.getAsInt(params, "levlSocWh"); + this.socLowerBoundPercent = JsonUtils.getAsDouble(params, "levlSocLowerBoundPercent"); + this.socUpperBoundPercent = JsonUtils.getAsDouble(params, "levlSocUpperBoundPercent"); + this.sellToGridLimitW = JsonUtils.getAsInt(params, "sellToGridLimitW"); + this.buyFromGridLimitW = JsonUtils.getAsInt(params, "buyFromGridLimitW"); + this.efficiencyPercent = JsonUtils.getAsDouble(params, "efficiencyPercent"); + this.influenceSellToGrid = JsonUtils.getAsBoolean(params, "influenceSellToGrid"); } @Override diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java index 816c9fbb224..35817bc5e51 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java @@ -1,39 +1,53 @@ package io.openems.edge.levl.controller.common; public class Efficiency { - + /** - * Applies an efficiency to a power/energy outside of the battery. - * negative values for Charge; positive for Discharge + * Applies an efficiency to a power/energy outside of the battery. + *

+ * Negative values for charge; positive for discharge + *

* - * @param value power/energy to which the efficiency should be applied + * @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) { + public static long apply(long value, double efficiencyPercent) { if (value <= 0) { // charge - return Math.round(value * efficiencyPercent / 100); + return multiplyEfficiency(value, efficiencyPercent); } - + // discharge - return Math.round(value / (efficiencyPercent / 100)); + return divideEfficiency(value, efficiencyPercent); } - + /** - * Unapplies an efficiency to a power/energy inside of the battery. - * negative values for Charge; positive for Discharge + * Unapplies an efficiency to a power/energy inside of the battery. * - * @param value power/energy to which the efficiency should be applied - * @param efficiencyPercent efficiency which should be applied + *

+ * 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) { + public static long unapply(long value, double efficiencyPercent) { if (value <= 0) { // charge - return Math.round(value / (efficiencyPercent / 100)); + return divideEfficiency(value, efficiencyPercent); } - + // discharge + return multiplyEfficiency(value, efficiencyPercent); + } + + private static long divideEfficiency(long value, double efficiencyPercent) { + return Math.round(value / (efficiencyPercent / 100)); + } + + private static long multiplyEfficiency(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/io/openems/edge/levl/controller/BalancingImplTest.java b/io.openems.edge.levl.controller/test/io/openems/edge/levl/controller/BalancingImplTest.java index 79c3d2b8bf7..c504874eb36 100644 --- 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 @@ -18,836 +18,865 @@ public class BalancingImplTest { 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 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, "LevlSoc"); + 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, "SocLowerBoundLevl"); - private static final ChannelAddress SOC_UPPER_BOUND_LEVL = new ChannelAddress(CTRL_ID, "SocUpperBoundLevl"); - private static final ChannelAddress LEVL_INFLUENCE_SELL_TO_GRID = new ChannelAddress(CTRL_ID, "InfluenceSellToGrid"); - private static final ChannelAddress LEVL_EFFICIENCY = new ChannelAddress(CTRL_ID, "Efficiency"); - private static final ChannelAddress PUC_BATTERY_POWER = new ChannelAddress(CTRL_ID, "PucBatteryPower"); - + 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 { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)); + .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 { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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) + .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)); + .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 { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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) + .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)); + .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 with discharge request (ws) > MAX_INT. Constrained by sell to grid + // limit. @Test public void testWithLargeLevlDischargeRequest() throws Exception { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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) + .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)); + .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 { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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_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(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) - .output(PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl - .output(LEVL_REMAINING_LEVL_ENERGY, -9_500_000L) // 500,000 Ws should be realized, therefore 9,500,000 Ws are remaining - .output(LEVL_SOC, -179_600_000L)) // Levl soc increases by 500,000 Ws * 80% efficiency = 400,000 Ws - .next(new TestCase() - .input(ESS_SOC, 90) // 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_ACTIVE_POWER, -500_000) + .input(DEBUG_SET_ACTIVE_POWER, -500_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // + .output(PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely + // reserved for Levl + .output(LEVL_REMAINING_LEVL_ENERGY, -9_500_000L) // 500,000 Ws should be realized, therefore + // 9,500,000 Ws are remaining + .output(LEVL_SOC, -179_600_000L)) // Levl soc increases by 500,000 Ws * 80% efficiency = 400,000 + // Ws + .next(new TestCase() // + .input(ESS_SOC, 90) // 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_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) - .output(PUC_BATTERY_POWER, -20_000L) // since reserved capacity decreased by 400,000 Ws in the previous cycle but ess soc value remains the same, puc can charge again - .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // 480,000 Ws can be realized for Levl, therefore 9,020,00 are remaining - .output(LEVL_SOC, -179_216_000L)); // Levl soc increases by 480,000 Ws * 80% efficiency = 384,000 Ws + .input(DEBUG_SET_ACTIVE_POWER, -500_000) // + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // + .output(PUC_BATTERY_POWER, -20_000L) // since reserved capacity decreased by 400,000 Ws in the + // previous cycle but ess soc value remains the same, + // puc can charge again + .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // 480,000 Ws can be realized for Levl, + // therefore 9,020,00 are remaining + .output(LEVL_SOC, -179_216_000L)); // Levl soc increases by 480,000 Ws * 80% efficiency = + // 384,000 Ws } - + @Test public void testWithReservedChargeCapacityLevlDischargesPucMustNotCharge() throws Exception { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() + .withMaxApparentPower(500_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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_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(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) - .output(PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely reserved for Levl - .output(LEVL_REMAINING_LEVL_ENERGY, 9_500_000L) // 500,000 Ws should be realized, therefore 9,500,000 Ws are remaining - .output(LEVL_SOC, -180_625_000L)) // Levl soc decreases by 500,000 Ws / 80% efficiency = 625,000 Ws - .next(new TestCase() - .input(ESS_SOC, 90) // 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_ACTIVE_POWER, 500_000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 500_000) // + .output(PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely + // reserved for Levl + .output(LEVL_REMAINING_LEVL_ENERGY, 9_500_000L) // 500,000 Ws should be realized, therefore + // 9,500,000 Ws are remaining + .output(LEVL_SOC, -180_625_000L)) // Levl soc decreases by 500,000 Ws / 80% efficiency = 625,000 + // Ws + .next(new TestCase() // + .input(ESS_SOC, 90) // 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_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) - .output(PUC_BATTERY_POWER, 0L) // // puc should not do anything because capacity is still completely reserved for Levl - .output(LEVL_REMAINING_LEVL_ENERGY, 9_000_000L) // 500,000 Ws should be realized, therefore 9,000,000 Ws are remaining - .output(LEVL_SOC, -181_250_000L)); // Levl soc decreases by 500,000 Ws / 80% efficiency = 625,000 Ws + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, 500_000) // + .output(PUC_BATTERY_POWER, 0L) // // puc should not do anything because capacity is still + // completely reserved for Levl + .output(LEVL_REMAINING_LEVL_ENERGY, 9_000_000L) // 500,000 Ws should be realized, therefore + // 9,000,000 Ws are remaining + .output(LEVL_SOC, -181_250_000L)); // Levl soc decreases by 500,000 Ws / 80% efficiency = + // 625,000 Ws } - - + @Test public void testWithReservedChargeCapacityLevlChargesPucMayCharge() throws Exception { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() + .withMaxApparentPower(500_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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_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(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) - .output(PUC_BATTERY_POWER, -10_000L) // puc should charge 10,000 Ws since 5% capacity is available - .output(LEVL_REMAINING_LEVL_ENERGY, -9_510_000L) // 490,000 Ws should be realized, therefore 9,510,000 Ws are remaining - .output(LEVL_SOC, -179_608_000L)) // Levl soc increases by 490,000 Ws * 80% efficiency = 392,000 Ws - .next(new TestCase() - .input(ESS_SOC, 85) // 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_ACTIVE_POWER, -500_000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // + .output(PUC_BATTERY_POWER, -10_000L) // puc should charge 10,000 Ws since 5% capacity is + // available + .output(LEVL_REMAINING_LEVL_ENERGY, -9_510_000L) // 490,000 Ws should be realized, therefore + // 9,510,000 Ws are remaining + .output(LEVL_SOC, -179_608_000L)) // Levl soc increases by 490,000 Ws * 80% efficiency = 392,000 + // Ws + .next(new TestCase() // + .input(ESS_SOC, 85) // 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_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(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // .output(PUC_BATTERY_POWER, -10_000L) // puc should still charge 10,000 Ws - .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // 490,000 Ws should be realized, therefore 9,020,000 Ws are remaining - .output(LEVL_SOC, -179_216_000L)); // Levl soc increases by 490,000 Ws * 80% efficiency = 392,000 Ws + .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // 490,000 Ws should be realized, therefore + // 9,020,000 Ws are remaining + .output(LEVL_SOC, -179_216_000L)); // Levl soc increases by 490,000 Ws * 80% efficiency = + // 392,000 Ws } - + @Test public void testWithReservedChargeCapacityLevlChargesPucMayDischarge() throws Exception { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() + .withMaxApparentPower(500_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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_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(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) - .output(PUC_BATTERY_POWER, 10_000L) // puc should discharge 10,000 Ws since Levl has reserved charge not discharge energy - .output(LEVL_REMAINING_LEVL_ENERGY, -9_490_000L) // 510,000 Ws can be realized, because puc discharges 10,000 Ws - .output(LEVL_SOC, -179_592_000L)) // Levl soc increases by 510,000 Ws * 80% efficiency = 408,000 Ws - .next(new TestCase() - .input(ESS_SOC, 50) // 50% = 250,000 Wh = 900,000,000 Ws | should be 900,400,000 Ws but only full percent values can be read - .input(ESS_ACTIVE_POWER, -500_000) + .output(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // + .output(PUC_BATTERY_POWER, 10_000L) // puc should discharge 10,000 Ws since Levl has reserved + // charge not discharge energy + .output(LEVL_REMAINING_LEVL_ENERGY, -9_490_000L) // 510,000 Ws can be realized, because puc + // discharges 10,000 Ws + .output(LEVL_SOC, -179_592_000L)) // Levl soc increases by 510,000 Ws * 80% efficiency = 408,000 + // Ws + .next(new TestCase() // + .input(ESS_SOC, 50) // 50% = 250,000 Wh = 900,000,000 Ws | should be 900,400,000 Ws but only + // full percent values can be read + .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(ESS_SET_ACTIVE_POWER_EQUALS_WITH_PID, -500_000) // .output(PUC_BATTERY_POWER, 10_000L) // puc should still charge 10,000 Ws - .output(LEVL_REMAINING_LEVL_ENERGY, -8_980_000L) // 510,000 Ws can be realized again, therefore 8,980,000 Ws are remaining - .output(LEVL_SOC, -179_184_000L)); // Levl soc increases by 510,000 Ws * 80% efficiency = 408,000 Ws + .output(LEVL_REMAINING_LEVL_ENERGY, -8_980_000L) // 510,000 Ws can be realized again, therefore + // 8,980,000 Ws are remaining + .output(LEVL_SOC, -179_184_000L)); // Levl soc increases by 510,000 Ws * 80% efficiency = + // 408,000 Ws } - - + @Test public void testInfluenceSellToGrid_PucSellToGrid_LevlChargeForbidden() throws Exception { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .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)); + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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 { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .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)); + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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 { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .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)); - } - + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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 { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .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)); + .withMaxApparentPower(500000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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 { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() + .withMaxApparentPower(20_000_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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) + .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)); + .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 { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() + .withMaxApparentPower(30_000_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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_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)); + .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 { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() + .withMaxApparentPower(20_000_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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) + .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)); + .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 { - new ControllerTest(new ControllerEssBalancingImpl()) - .addReference("cm", new DummyConfigurationAdmin()) - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) - .setPower(new DummyPower(0.3, 0.3, 0.1)) + new ControllerTest(new ControllerEssBalancingImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .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)) - .activate(MyConfig.create() - .setId(CTRL_ID) - .setEssId(ESS_ID) - .setMeterId(METER_ID) - .build()) - .next(new TestCase() + .withMaxApparentPower(30_000_000)) // + .addReference("meter", new DummyElectricityMeter(METER_ID)) // + .addReference("cycle", new DummyCycle(1000)) // + .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .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_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)); + .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 index 1163a6abfa3..7ada826929f 100644 --- 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 @@ -45,7 +45,7 @@ public void testCalculateRequiredPower() throws OpenemsNamedException { this.setActiveChannelValue(this.underTest.getLevlSocChannel(), 2000L); this.setActiveChannelValue(this.underTest.getRemainingLevlEnergyChannel(), 200000L); - this.setActiveChannelValue(this.underTest.getEfficiencyChannel(), 100.0); + 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); @@ -260,7 +260,7 @@ public void testHandleEvent_after() { this.setNextChannelValue(this.underTest.getPucBatteryPowerChannel(), 10L); this.setActiveChannelValue(this.underTest.getLevlSocChannel(), 40L); this.setActiveChannelValue(this.underTest.getRemainingLevlEnergyChannel(), -1000L); - this.setActiveChannelValue(this.underTest.getEfficiencyChannel(), 80.0); + this.setActiveChannelValue(this.underTest.getEssEfficiencyChannel(), 80.0); this.underTest.currentRequest = new LevlControlRequest(); this.setActiveChannelValue(this.underTest.getRealizedEnergyGridChannel(), -20L); From 72cff762f47cd6ab9304f147ac8ab48a879e0167 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Mon, 18 Nov 2024 10:31:39 +0100 Subject: [PATCH 36/41] levl-1329/1332: fix checkstyle --- .../ControllerEssBalancingImpl.java | 8 +- .../levl/controller/BalancingImplTest.java | 127 +++++++++--------- 2 files changed, 71 insertions(+), 64 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index f12c19dbde8..3724c8ebdc9 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -287,7 +287,7 @@ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, powerUpperBound = 0; } - return (int) applyBound(pucPower, powerLowerBound, powerUpperBound); + return (int) this.applyBound(pucPower, powerLowerBound, powerUpperBound); } /** @@ -366,7 +366,7 @@ protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBattery int maxEssPower) { var levlPowerLowerBound = Long.valueOf(minEssPower) - pucBatteryPower; var levlPowerUpperBound = Long.valueOf(maxEssPower) - pucBatteryPower; - return applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); + return this.applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); } /** @@ -402,7 +402,7 @@ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, var levlPowerLowerBound = Efficiency.unapply(round(levlDischargeEnergyLowerBoundWs / cycleTimeS), efficiency); var levlPowerUpperBound = Efficiency.unapply(round(levlDischargeEnergyUpperBoundWs / cycleTimeS), efficiency); - return applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); + return this.applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); } /** @@ -418,7 +418,7 @@ protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, long sellToGridLimit) { var levlPowerLowerBound = -(buyFromGridLimit - pucGridPower); var levlPowerUpperBound = -(sellToGridLimit - pucGridPower); - return applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); + return this.applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); } /** 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 index c504874eb36..3e7040dca7d 100644 --- 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 @@ -30,8 +30,10 @@ public class BalancingImplTest { 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 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"); @@ -305,26 +307,26 @@ public void testWithReservedChargeCapacityLevlChargesPucMustNotCharge() throws E .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) // - .output(PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely - // reserved for Levl - .output(LEVL_REMAINING_LEVL_ENERGY, -9_500_000L) // 500,000 Ws should be realized, therefore - // 9,500,000 Ws are remaining - .output(LEVL_SOC, -179_600_000L)) // Levl soc increases by 500,000 Ws * 80% efficiency = 400,000 - // Ws - .next(new TestCase() // - .input(ESS_SOC, 90) // 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_ACTIVE_POWER, -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) // - .output(PUC_BATTERY_POWER, -20_000L) // since reserved capacity decreased by 400,000 Ws in the - // previous cycle but ess soc value remains the same, - // puc can charge again - .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // 480,000 Ws can be realized for Levl, - // therefore 9,020,00 are remaining - .output(LEVL_SOC, -179_216_000L)); // Levl soc increases by 480,000 Ws * 80% efficiency = - // 384,000 Ws + // 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 @@ -359,25 +361,27 @@ public void testWithReservedChargeCapacityLevlDischargesPucMustNotCharge() throw .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) // - .output(PUC_BATTERY_POWER, 0L) // puc should not do anything because capacity is completely - // reserved for Levl - .output(LEVL_REMAINING_LEVL_ENERGY, 9_500_000L) // 500,000 Ws should be realized, therefore - // 9,500,000 Ws are remaining - .output(LEVL_SOC, -180_625_000L)) // Levl soc decreases by 500,000 Ws / 80% efficiency = 625,000 - // Ws - .next(new TestCase() // - .input(ESS_SOC, 90) // 90% = 450,000 Wh = 1,620,000,000 Ws | should be 1,619,375,000 Ws but only - // full percent values can be read + // 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) // - .output(PUC_BATTERY_POWER, 0L) // // puc should not do anything because capacity is still - // completely reserved for Levl - .output(LEVL_REMAINING_LEVL_ENERGY, 9_000_000L) // 500,000 Ws should be realized, therefore - // 9,000,000 Ws are remaining - .output(LEVL_SOC, -181_250_000L)); // Levl soc decreases by 500,000 Ws / 80% efficiency = - // 625,000 Ws + // 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 @@ -412,24 +416,25 @@ public void testWithReservedChargeCapacityLevlChargesPucMayCharge() throws Excep .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) // - .output(PUC_BATTERY_POWER, -10_000L) // puc should charge 10,000 Ws since 5% capacity is - // available - .output(LEVL_REMAINING_LEVL_ENERGY, -9_510_000L) // 490,000 Ws should be realized, therefore - // 9,510,000 Ws are remaining - .output(LEVL_SOC, -179_608_000L)) // Levl soc increases by 490,000 Ws * 80% efficiency = 392,000 - // Ws - .next(new TestCase() // - .input(ESS_SOC, 85) // 85% = 425,000 Wh = 1,530,000,000 Ws | should be 1,530,400,000 Ws but only - // full percent values can be read + // 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 - .output(LEVL_REMAINING_LEVL_ENERGY, -9_020_000L) // 490,000 Ws should be realized, therefore - // 9,020,000 Ws are remaining - .output(LEVL_SOC, -179_216_000L)); // Levl soc increases by 490,000 Ws * 80% efficiency = - // 392,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 @@ -464,24 +469,26 @@ public void testWithReservedChargeCapacityLevlChargesPucMayDischarge() throws Ex .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) // - .output(PUC_BATTERY_POWER, 10_000L) // puc should discharge 10,000 Ws since Levl has reserved - // charge not discharge energy - .output(LEVL_REMAINING_LEVL_ENERGY, -9_490_000L) // 510,000 Ws can be realized, because puc - // discharges 10,000 Ws - .output(LEVL_SOC, -179_592_000L)) // Levl soc increases by 510,000 Ws * 80% efficiency = 408,000 - // Ws - .next(new TestCase() // - .input(ESS_SOC, 50) // 50% = 250,000 Wh = 900,000,000 Ws | should be 900,400,000 Ws but only - // full percent values can be read + // 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 - .output(LEVL_REMAINING_LEVL_ENERGY, -8_980_000L) // 510,000 Ws can be realized again, therefore - // 8,980,000 Ws are remaining - .output(LEVL_SOC, -179_184_000L)); // Levl soc increases by 510,000 Ws * 80% efficiency = - // 408,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 From bf3e9f93d3753387a56c99a8c57d15e6b771e1e5 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Thu, 21 Nov 2024 15:21:58 +0100 Subject: [PATCH 37/41] levl-1329/1332: minor review adjustments --- .../controller/ControllerEssBalancingImpl.java | 6 +++--- .../edge/levl/controller/LevlControlRequest.java | 16 +++++++++------- .../edge/levl/controller/common/Efficiency.java | 12 ++++++------ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 3724c8ebdc9..939ab92e809 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -252,6 +252,7 @@ protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocW // apply ess power limits pucBatteryPower = max(min(pucBatteryPower, maxEssPower), minEssPower); + pucBatteryPower = (int) this.applyBound(pucBatteryPower, minEssPower, maxEssPower); this.log.debug("pucBatteryPower with ess power limits: " + pucBatteryPower); // apply soc bounds @@ -273,11 +274,10 @@ protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocW */ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, double efficiency, double cycleTimeS) { - // efficiency = 50 var dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; var dischargeEnergyUpperBoundWs = pucSocWs; - var powerLowerBound = Efficiency.unapply(round(dischargeEnergyLowerBoundWs / cycleTimeS), efficiency); + var powerLowerBound = Efficiency.unapply(round(dischargeEnergyLowerBoundWs / cycleTimeS), efficiency); var powerUpperBound = Efficiency.unapply(round(dischargeEnergyUpperBoundWs / cycleTimeS), efficiency); if (powerLowerBound > 0) { @@ -586,7 +586,7 @@ private void startNextRequest() { private boolean hasSignChanged(long a, long b) { return a < 0 && b > 0 || a > 0 && b < 0; } - + private long applyBound(long power, long lowerBound, long upperBound) { return max(min(power, upperBound), lowerBound); } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index b8a1b4fce1c..b31c398c497 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -27,11 +27,8 @@ public class LevlControlRequest { protected double efficiencyPercent; protected boolean influenceSellToGrid; - public LevlControlRequest() { - - } - public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedException { + protected LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedException { try { this.parseFields(params); } catch (NullPointerException e) { @@ -46,9 +43,14 @@ public LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedExc throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("efficiencyPercent must be <= 100"); } } + + //Just for testing + protected LevlControlRequest() { + + } //Just for testing - public LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String levlRequestId, String timestamp, + protected LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String levlRequestId, String timestamp, long energyWs, LocalDateTime start, LocalDateTime deadline, int levlSocWh, int socLowerBoundPercent, int socUpperBoundPercent, double efficiencyPercent, boolean influenceSellToGrid) { this.sellToGridLimitW = sellToGridLimitW; @@ -66,7 +68,7 @@ public LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String le } //Just for testing - public LevlControlRequest(int startDelay, int duration) { + protected LevlControlRequest(int startDelay, int duration) { this.start = LocalDateTime.now(LevlControlRequest.clock).plusSeconds(startDelay); this.deadline = this.start.plusSeconds(duration); } @@ -78,7 +80,7 @@ public LevlControlRequest(int startDelay, int duration) { * @return the levl control request * @throws OpenemsNamedException on error */ - public static LevlControlRequest from(JsonrpcRequest request) throws OpenemsNamedException { + protected static LevlControlRequest from(JsonrpcRequest request) throws OpenemsNamedException { var params = request.getParams(); return new LevlControlRequest(params); } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java index 35817bc5e51..7c79081a842 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java @@ -15,11 +15,11 @@ public class Efficiency { */ public static long apply(long value, double efficiencyPercent) { if (value <= 0) { // charge - return multiplyEfficiency(value, efficiencyPercent); + return multiplyByEfficiency(value, efficiencyPercent); } // discharge - return divideEfficiency(value, efficiencyPercent); + return divideByEfficiency(value, efficiencyPercent); } /** @@ -36,18 +36,18 @@ public static long apply(long value, double efficiencyPercent) { */ public static long unapply(long value, double efficiencyPercent) { if (value <= 0) { // charge - return divideEfficiency(value, efficiencyPercent); + return divideByEfficiency(value, efficiencyPercent); } // discharge - return multiplyEfficiency(value, efficiencyPercent); + return multiplyByEfficiency(value, efficiencyPercent); } - private static long divideEfficiency(long value, double efficiencyPercent) { + private static long divideByEfficiency(long value, double efficiencyPercent) { return Math.round(value / (efficiencyPercent / 100)); } - private static long multiplyEfficiency(long value, double efficiencyPercent) { + private static long multiplyByEfficiency(long value, double efficiencyPercent) { return Math.round(value * efficiencyPercent / 100); } } \ No newline at end of file From 78fc378f0a0166621ff90886d6d19d494a31451d Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Wed, 18 Dec 2024 08:49:03 +0100 Subject: [PATCH 38/41] levl-1404: move from systemClock to componentManager clock --- .../ControllerEssBalancingImpl.java | 20 ++-- .../levl/controller/LevlControlRequest.java | 31 +++--- .../levl/controller/BalancingImplTest.java | 97 ++++++++++++++++--- .../ControllerEssBalancingImplTest.java | 67 +++++++------ 4 files changed, 140 insertions(+), 75 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 939ab92e809..9dac47c1a11 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -1,7 +1,6 @@ package io.openems.edge.levl.controller; -import java.time.Clock; -import java.time.LocalDateTime; +import java.time.Instant; import java.util.UUID; import org.osgi.service.cm.ConfigurationAdmin; @@ -25,6 +24,7 @@ import io.openems.common.jsonrpc.base.JsonrpcResponse; import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.cycle.Cycle; import io.openems.edge.common.event.EdgeEventConstants; @@ -54,12 +54,13 @@ public class ControllerEssBalancingImpl extends AbstractOpenemsComponent private static final double MILLISECONDS_PER_SECOND = 1000.0; private static final String METHOD = "sendLevlControlRequest"; - protected static Clock clock = Clock.systemDefaultZone(); - private final Logger log = LoggerFactory.getLogger(ControllerEssBalancingImpl.class); @Reference private ConfigurationAdmin cm; + + @Reference + protected ComponentManager componentManager; @Reference protected ManagedSymmetricEss ess; @@ -458,7 +459,8 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) { * @throws OpenemsNamedException on error */ protected JsonrpcResponse handleRequest(Call call) throws OpenemsNamedException { - var request = LevlControlRequest.from(call.getRequest()); + var now = Instant.now(this.componentManager.getClock()); + var request = LevlControlRequest.from(call.getRequest(), now); this.log.info("Received new levl request: {}", request); this.nextRequest = request; var realizedEnergyBatteryWs = this.getRealizedEnergyBattery().getOrError(); @@ -477,8 +479,8 @@ private JsonObject generateResponse(UUID requestId, String levlRequestId) { return response; } - private static boolean isActive(LevlControlRequest request) { - LocalDateTime now = LocalDateTime.now(clock); + private boolean isActive(LevlControlRequest request) { + var now = Instant.now(this.componentManager.getClock()); return !(request == null || now.isBefore(request.start) || now.isAfter(request.deadline)); } @@ -486,12 +488,12 @@ private static boolean isActive(LevlControlRequest request) { public void handleEvent(Event event) { switch (event.getTopic()) { case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE -> { - if (isActive(this.nextRequest)) { + if (this.isActive(this.nextRequest)) { if (this.currentRequest != null) { this.finishRequest(); } this.startNextRequest(); - } else if (this.currentRequest != null && !isActive(this.currentRequest)) { + } else if (this.currentRequest != null && !this.isActive(this.currentRequest)) { this.finishRequest(); } } diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java index b31c398c497..1f8d164e910 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -5,22 +5,19 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.jsonrpc.base.JsonrpcRequest; import io.openems.common.utils.JsonUtils; - -import java.time.Clock; -import java.time.LocalDateTime; +import java.time.Instant; import java.util.Objects; -public class LevlControlRequest { +public class LevlControlRequest { public static final String METHOD = "sendLevlControlRequest"; public static final int QUARTER_HOUR_SECONDS = 900; - protected static Clock clock = Clock.systemDefaultZone(); protected int sellToGridLimitW; protected int buyFromGridLimitW; protected String levlRequestId; protected String timestamp; protected long energyWs; - protected LocalDateTime start; - protected LocalDateTime deadline; + protected Instant start; + protected Instant deadline; protected int levlSocWh; protected double socLowerBoundPercent; protected double socUpperBoundPercent; @@ -28,9 +25,9 @@ public class LevlControlRequest { protected boolean influenceSellToGrid; - protected LevlControlRequest(JsonObject params) throws OpenemsError.OpenemsNamedException { + protected LevlControlRequest(JsonObject params, Instant now) throws OpenemsError.OpenemsNamedException { try { - this.parseFields(params); + this.parseFields(params, now); } catch (NullPointerException e) { throw OpenemsError.JSONRPC_INVALID_MESSAGE.exception("missing fields in request: " + e.getMessage()); } catch (NumberFormatException e) { @@ -51,7 +48,7 @@ protected LevlControlRequest() { //Just for testing protected LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String levlRequestId, String timestamp, - long energyWs, LocalDateTime start, LocalDateTime deadline, int levlSocWh, int socLowerBoundPercent, + long energyWs, Instant start, Instant deadline, int levlSocWh, int socLowerBoundPercent, int socUpperBoundPercent, double efficiencyPercent, boolean influenceSellToGrid) { this.sellToGridLimitW = sellToGridLimitW; this.buyFromGridLimitW = buyFromGridLimitW; @@ -68,8 +65,8 @@ protected LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String } //Just for testing - protected LevlControlRequest(int startDelay, int duration) { - this.start = LocalDateTime.now(LevlControlRequest.clock).plusSeconds(startDelay); + protected LevlControlRequest(int startDelay, int duration, Instant now) { + this.start = now.plusSeconds(startDelay); this.deadline = this.start.plusSeconds(duration); } @@ -77,20 +74,20 @@ protected LevlControlRequest(int startDelay, int duration) { * Generates a levl control request object based on the JSON-RPC request. * * @param request the JSON-RPC request + * @param now the current time * @return the levl control request * @throws OpenemsNamedException on error */ - protected static LevlControlRequest from(JsonrpcRequest request) throws OpenemsNamedException { + protected static LevlControlRequest from(JsonrpcRequest request, Instant now) throws OpenemsNamedException { var params = request.getParams(); - return new LevlControlRequest(params); + return new LevlControlRequest(params, now); } - private void parseFields(JsonObject params) throws OpenemsNamedException { + private void parseFields(JsonObject params, Instant now) throws OpenemsNamedException { this.levlRequestId = JsonUtils.getAsString(params, "levlRequestId"); this.timestamp = JsonUtils.getAsString(params, "levlRequestTimestamp"); this.energyWs = JsonUtils.getAsLong(params, "levlPowerW") * QUARTER_HOUR_SECONDS; - this.start = LocalDateTime.now(LevlControlRequest.clock) - .plusSeconds(JsonUtils.getAsInt(params, "levlChargeDelaySec")); + this.start = now.plusSeconds(JsonUtils.getAsInt(params, "levlChargeDelaySec")); this.deadline = this.start.plusSeconds(JsonUtils.getAsInt(params, "levlChargeDurationSec")); this.levlSocWh = JsonUtils.getAsInt(params, "levlSocWh"); this.socLowerBoundPercent = JsonUtils.getAsDouble(params, "levlSocLowerBoundPercent"); 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 index 3e7040dca7d..d533a76e566 100644 --- 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 @@ -1,9 +1,14 @@ 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; @@ -41,8 +46,10 @@ public class BalancingImplTest { @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 @@ -116,8 +123,12 @@ public void testWithoutLevlRequest() throws Exception { @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 @@ -125,7 +136,7 @@ public void testWithLevlDischargeRequest() throws Exception { .withMaxApparentPower(500000)) // .addReference("meter", new DummyElectricityMeter(METER_ID)) // .addReference("cycle", new DummyCycle(1000)) // - .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -169,8 +180,12 @@ public void testWithLevlDischargeRequest() throws Exception { @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 @@ -178,7 +193,7 @@ public void testWithLevlChargeRequest() throws Exception { .withMaxApparentPower(500000)) // .addReference("meter", new DummyElectricityMeter(METER_ID)) // .addReference("cycle", new DummyCycle(1000)) // - .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -224,8 +239,12 @@ public void testWithLevlChargeRequest() throws Exception { // 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 @@ -233,7 +252,7 @@ public void testWithLargeLevlDischargeRequest() throws Exception { .withMaxApparentPower(500000)) // .addReference("meter", new DummyElectricityMeter(METER_ID)) // .addReference("cycle", new DummyCycle(1000)) // - .addReference("currentRequest", new LevlControlRequest(0, 100)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -277,15 +296,19 @@ public void testWithLargeLevlDischargeRequest() throws Exception { @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -331,15 +354,19 @@ public void testWithReservedChargeCapacityLevlChargesPucMustNotCharge() throws E @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -386,15 +413,19 @@ public void testWithReservedChargeCapacityLevlDischargesPucMustNotCharge() throw @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -439,15 +470,19 @@ public void testWithReservedChargeCapacityLevlChargesPucMayCharge() throws Excep @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -493,15 +528,19 @@ public void testWithReservedChargeCapacityLevlChargesPucMayDischarge() throws Ex @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -528,15 +567,19 @@ public void testInfluenceSellToGrid_PucSellToGrid_LevlChargeForbidden() throws E @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -563,15 +606,19 @@ public void testInfluenceSellToGrid_PucSellToGrid_LevlDischargeForbidden() throw @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -598,15 +645,19 @@ public void testInfluenceSellToGrid_PucBuyFromGrid_LevlChargeAllowed() throws Ex @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -633,15 +684,19 @@ public void testInfluenceSellToGrid_PucBuyFromGrid_LevlDischargeLimited() throws @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -688,15 +743,19 @@ public void testUpperSocLimit() throws Exception { @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -761,15 +820,19 @@ public void testUpperSocLimit_levlHasCharged() throws Exception { @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // @@ -816,15 +879,19 @@ public void testLowerSocLimit() throws Exception { @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)) // + .addReference("currentRequest", new LevlControlRequest(0, 100, now)) // .activate(MyConfig.create() // .setId(CTRL_ID) // .setEssId(ESS_ID) // 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 index 7ada826929f..84f19333527 100644 --- 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 @@ -1,9 +1,9 @@ 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.time.LocalDateTime; -import java.time.ZoneOffset; import java.util.HashMap; import org.junit.Assert; @@ -20,6 +20,7 @@ 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; @@ -138,17 +139,17 @@ public void testInfluenceSellToGridConstraint() { @Test public void testHandleEvent_before_currentActive() { - Clock clock = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); // 2024-10-24T14:00:00 - ControllerEssBalancingImpl.clock = clock; + Clock clock = createDummyClock(); + this.underTest.componentManager = new DummyComponentManager(clock); LevlControlRequest currentRequest = new LevlControlRequest(); - currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); - currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); + 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 = LocalDateTime.of(2024, 10, 24, 14, 15, 0); - nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 29, 59); + 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<>()); @@ -160,18 +161,17 @@ public void testHandleEvent_before_currentActive() { @Test public void testHandleEvent_before_nextRequestIsActive() { - // 2024-10-24T14:15:00 - Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); - ControllerEssBalancingImpl.clock = clock; + Clock clock = createDummyClock(); + this.underTest.componentManager = new DummyComponentManager(clock); LevlControlRequest currentRequest = new LevlControlRequest(); - currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); - currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); + 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 = LocalDateTime.of(2024, 10, 24, 14, 15, 0); - nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 29, 59); + 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); @@ -189,17 +189,17 @@ public void testHandleEvent_before_nextRequestIsActive() { @Test public void testHandleEvent_before_gapBetweenRequests() { - Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); // 2024-10-24T14:15:00 - ControllerEssBalancingImpl.clock = clock; + Clock clock = createDummyClock(); + this.underTest.componentManager = new DummyComponentManager(clock); LevlControlRequest currentRequest = new LevlControlRequest(); - currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); - currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); + 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 = LocalDateTime.of(2024, 10, 24, 14, 16, 0); - nextRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 29, 59); + 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); @@ -217,13 +217,12 @@ public void testHandleEvent_before_gapBetweenRequests() { @Test public void testHandleEvent_before_noNextRequest() { - // 2024-10-24T14:15:00 - Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); - ControllerEssBalancingImpl.clock = clock; + Clock clock = createDummyClock(); + this.underTest.componentManager = new DummyComponentManager(clock); LevlControlRequest currentRequest = new LevlControlRequest(); - currentRequest.start = LocalDateTime.of(2024, 10, 24, 14, 0, 0); - currentRequest.deadline = LocalDateTime.of(2024, 10, 24, 14, 14, 59); + 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); @@ -243,8 +242,8 @@ public void testHandleEvent_before_noNextRequest() { public void testHandleEvent_before_noRequests() { Event event = new Event(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, new HashMap<>()); - Clock clock = Clock.fixed(Instant.ofEpochSecond(1729779300), ZoneOffset.UTC); // 2024-10-24T14:15:00 - ControllerEssBalancingImpl.clock = clock; + Clock clock = createDummyClock(); + this.underTest.componentManager = new DummyComponentManager(clock); this.underTest.handleEvent(event); @@ -280,10 +279,10 @@ public void testHandleEvent_after() { public void testHandleRequest() throws OpenemsNamedException { JsonObject params = new JsonObject(); params.addProperty("levlRequestId", "id"); - params.addProperty("levlRequestTimestamp", "2024-10-24T14:15:00Z"); + params.addProperty("levlRequestTimestamp", "2020-01-01T00:15:00Z"); params.addProperty("levlPowerW", 500); params.addProperty("levlChargeDelaySec", 900); - params.addProperty("levlChargeDurationSec", 900); + params.addProperty("levlChargeDurationSec", 899); params.addProperty("levlSocWh", 10000); params.addProperty("levlSocLowerBoundPercent", 20); params.addProperty("levlSocUpperBoundPercent", 80); @@ -294,10 +293,10 @@ public void testHandleRequest() throws OpenemsNamedException { JsonrpcRequest request = new GenericJsonrpcRequest("sendLevlControlRequest", params); Call call = new Call(request); - Clock clock = Clock.fixed(Instant.ofEpochSecond(1729778400), ZoneOffset.UTC); // 2024-10-24T14:00:00 - LevlControlRequest.clock = clock; - LevlControlRequest expectedNextRequest = new LevlControlRequest(3000, 4000, "id", "2024-10-24T14:15:00Z", - (500 * 900), LocalDateTime.of(2024, 10, 24, 14, 15, 0), LocalDateTime.of(2024, 10, 24, 14, 30, 0), + Clock clock = createDummyClock(); + this.underTest.componentManager = new DummyComponentManager(clock); + LevlControlRequest expectedNextRequest = new LevlControlRequest(3000, 4000, "id", "2020-01-01T00:15:00Z", + (500 * 900), Instant.now(clock).plusSeconds(900) /*2020-01-01T00:15:00*/, Instant.now(clock).plusSeconds(900 + 899) /*2020-01-01T00:29:59*/, 10000, 20, 80, 90, true); this.setActiveChannelValue(this.underTest.getRealizedEnergyBatteryChannel(), -100L); From c203b7a36066f4718443d8d06a527247023432d4 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Wed, 18 Dec 2024 09:06:46 +0100 Subject: [PATCH 39/41] levl-1404: add fitWithin method for long values and use them instead of private class method --- .../io/openems/edge/common/type/TypeUtils.java | 13 +++++++++++++ .../controller/ControllerEssBalancingImpl.java | 17 ++++++----------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java b/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java index d43a8bc6716..5e20a4fe360 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java +++ b/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java @@ -881,6 +881,19 @@ public static int fitWithin(int lowLimit, int highLimit, int value) { return Math.max(lowLimit, // Math.min(highLimit, value)); } + + /** + * Fits a value within a lower and upper boundary. + * + * @param lowLimit the long lower boundary + * @param highLimit the long upper boundary + * @param value the long actual value + * @return the adjusted long value + */ + public static long fitWithin(long lowLimit, long highLimit, long value) { + return Math.max(lowLimit, // + Math.min(highLimit, value)); + } /** * Fits a value within a lower and upper boundary. diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 9dac47c1a11..361cb45320c 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -31,6 +31,7 @@ import io.openems.edge.common.jsonapi.Call; import io.openems.edge.common.jsonapi.ComponentJsonApi; import io.openems.edge.common.jsonapi.JsonApiBuilder; +import io.openems.edge.common.type.TypeUtils; import io.openems.edge.controller.api.Controller; import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.ess.power.api.Phase; @@ -40,7 +41,6 @@ import static java.lang.Math.round; import static java.lang.Math.min; -import static java.lang.Math.max; @Designate(ocd = Config.class, factory = true) @Component(name = "Controller.Levl.Symmetric.Balancing", immediate = true, configurationPolicy = ConfigurationPolicy.REQUIRE) @@ -252,8 +252,7 @@ protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocW this.log.debug("pucBatteryPower without limits: " + pucBatteryPower); // apply ess power limits - pucBatteryPower = max(min(pucBatteryPower, maxEssPower), minEssPower); - pucBatteryPower = (int) this.applyBound(pucBatteryPower, minEssPower, maxEssPower); + pucBatteryPower = TypeUtils.fitWithin(minEssPower, maxEssPower, pucBatteryPower); this.log.debug("pucBatteryPower with ess power limits: " + pucBatteryPower); // apply soc bounds @@ -288,7 +287,7 @@ protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, powerUpperBound = 0; } - return (int) this.applyBound(pucPower, powerLowerBound, powerUpperBound); + return (int) TypeUtils.fitWithin(powerLowerBound, powerUpperBound, pucPower); } /** @@ -367,7 +366,7 @@ protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBattery int maxEssPower) { var levlPowerLowerBound = Long.valueOf(minEssPower) - pucBatteryPower; var levlPowerUpperBound = Long.valueOf(maxEssPower) - pucBatteryPower; - return this.applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); + return TypeUtils.fitWithin(levlPowerLowerBound, levlPowerUpperBound, levlPower); } /** @@ -403,7 +402,7 @@ protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, var levlPowerLowerBound = Efficiency.unapply(round(levlDischargeEnergyLowerBoundWs / cycleTimeS), efficiency); var levlPowerUpperBound = Efficiency.unapply(round(levlDischargeEnergyUpperBoundWs / cycleTimeS), efficiency); - return this.applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); + return TypeUtils.fitWithin(levlPowerLowerBound, levlPowerUpperBound, levlPower); } /** @@ -419,7 +418,7 @@ protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, long sellToGridLimit) { var levlPowerLowerBound = -(buyFromGridLimit - pucGridPower); var levlPowerUpperBound = -(sellToGridLimit - pucGridPower); - return this.applyBound(levlPower, levlPowerLowerBound, levlPowerUpperBound); + return TypeUtils.fitWithin(levlPowerLowerBound, levlPowerUpperBound, levlPower); } /** @@ -588,8 +587,4 @@ private void startNextRequest() { private boolean hasSignChanged(long a, long b) { return a < 0 && b > 0 || a > 0 && b < 0; } - - private long applyBound(long power, long lowerBound, long upperBound) { - return max(min(power, upperBound), lowerBound); - } } From 7514d4d8b244b88abf8b5a8f2e60e754cd9e03c3 Mon Sep 17 00:00:00 2001 From: Dennis Eitle Date: Wed, 18 Dec 2024 09:11:24 +0100 Subject: [PATCH 40/41] levl-1404: adjust code formatting to align with code convention --- .../controller/ControllerEssBalancingImpl.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java index 361cb45320c..199c16fb36b 100644 --- a/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -43,8 +43,16 @@ import static java.lang.Math.min; @Designate(ocd = Config.class, factory = true) -@Component(name = "Controller.Levl.Symmetric.Balancing", immediate = true, configurationPolicy = ConfigurationPolicy.REQUIRE) -@EventTopics({ EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, }) +@Component(// + name = "Controller.Levl.Symmetric.Balancing", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) + +@EventTopics({ // + EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, // + EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE, // +}) public class ControllerEssBalancingImpl extends AbstractOpenemsComponent implements Controller, OpenemsComponent, ControllerEssBalancing, ComponentJsonApi, EventHandler { @@ -58,7 +66,7 @@ public class ControllerEssBalancingImpl extends AbstractOpenemsComponent @Reference private ConfigurationAdmin cm; - + @Reference protected ComponentManager componentManager; From 805b31eec3b6a5869506443817488671c12d11ef Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Tue, 7 Jan 2025 16:34:57 +0100 Subject: [PATCH 41/41] apply tools/prepare-commit.sh (still failing) --- io.openems.edge.levl.controller/.classpath | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io.openems.edge.levl.controller/.classpath b/io.openems.edge.levl.controller/.classpath index bbfbdbe40e7..b4cffd0fe60 100644 --- a/io.openems.edge.levl.controller/.classpath +++ b/io.openems.edge.levl.controller/.classpath @@ -1,7 +1,7 @@ - +