diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index c1afa91dad3..ab52ca91873 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -148,6 +148,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',\ @@ -328,6 +329,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,\ 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/.classpath b/io.openems.edge.levl.controller/.classpath new file mode 100644 index 00000000000..b4cffd0fe60 --- /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..83f7ea62672 --- /dev/null +++ b/io.openems.edge.levl.controller/bnd.bnd @@ -0,0 +1,16 @@ +Bundle-Name: Ess Balancing with flexibility trading +Bundle-Vendor: Levl Energy GmbH +Bundle-License: https://opensource.org/licenses/EPL-2.0 +Bundle-Version: 0.1.0.${tstamp} + +-buildpath: \ + ${buildpath},\ + io.openems.common,\ + io.openems.edge.common,\ + io.openems.edge.controller.api,\ + io.openems.edge.ess.api,\ + io.openems.edge.ess.generic,\ + 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..bd009f0d5af --- /dev/null +++ b/io.openems.edge.levl.controller/readme.adoc @@ -0,0 +1,19 @@ += Levl Controller for ESS Balancing + +This is a beta version. + +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 +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.). + +== Requirements +The following components are required to use this controller successfully: +- Contract with Levl Energy for system optimization +- 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 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..cf22d48ded0 --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/Config.java @@ -0,0 +1,33 @@ +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 = "Combines the optimization of self-consumption with flexibility trading by levl.") +@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 = "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 "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 new file mode 100644 index 00000000000..ca595d7fdc2 --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancing.java @@ -0,0 +1,462 @@ +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.DoubleReadChannel; +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; +import io.openems.edge.controller.api.Controller; + +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_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; + + 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); + } + + /** + * 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_STATE_OF_CHARGE); + } + + /** + * 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 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 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); + } + + /** + * Returns the DoubleReadChannel for the lower soc bound. + * + * @return the DoubleReadChannel + */ + public default DoubleReadChannel getSocLowerBoundLevlChannel() { + return this.channel(ChannelId.STATE_OF_CHARGE_LOWER_BOUND_LEVL); + } + + /** + * Returns the value of the lower soc bound. + * + * @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. + * + * @param value the next value + */ + public default void _setSocLowerBoundLevl(Double value) { + this.getSocLowerBoundLevlChannel().setNextValue(value); + } + + /** + * Returns the DoubleReadChannel for the upper soc bound. + * + * @return the DoubleReadChannel + */ + public default DoubleReadChannel getSocUpperBoundLevlChannel() { + return this.channel(ChannelId.STATE_OF_CHARGE_UPPER_BOUND_LEVL); + } + + /** + * Returns the value of the upper soc bound. + * + * @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. + * + * @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 ess efficiency. + * + * @return the DoubleReadChannel + */ + public default DoubleReadChannel getEssEfficiencyChannel() { + return this.channel(ChannelId.ESS_EFFICIENCY); + } + + /** + * Returns the value of the ess efficiency. + * + * @return the value of the ess efficiency + */ + public default Value getEssEfficiency() { + return this.getEssEfficiencyChannel().value(); + } + + /** + * Sets the next value of the ess efficiency. + * + * @param value the next value + */ + public default void _setEssEfficiency(Double value) { + this.getEssEfficiencyChannel().setNextValue(value); + } + + /** + * Returns the LongReadChannel for the puc (primary use case) battery power. + * + * @return the LongReadChannel + */ + public default LongReadChannel getPucBatteryPowerChannel() { + return this.channel(ChannelId.PRIMARY_USE_CASE_BATTERY_POWER); + } + + /** + * Returns the value of the puc (primary use case) 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 (primary use case) battery power. + * + * @param value the next value + */ + public default void _setPucBatteryPower(Long value) { + this.getPucBatteryPowerChannel().setNextValue(value); + } + + /** + * 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() { + 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(); + } + + /** + * 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 (last request). + * + * @return the StringReadChannel + */ + public default StringReadChannel getLastRequestTimestampChannel() { + 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(); + } + + /** + * 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); + } + +} \ 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..199c16fb36b --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/ControllerEssBalancingImpl.java @@ -0,0 +1,598 @@ +package io.openems.edge.levl.controller; + +import java.time.Instant; +import java.util.UUID; + +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.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.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.ComponentManager; +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.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; +import io.openems.edge.ess.power.api.Pwr; +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; + +@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, // +}) + +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"; + + private final Logger log = LoggerFactory.getLogger(ControllerEssBalancingImpl.class); + + @Reference + private ConfigurationAdmin cm; + + @Reference + protected ComponentManager componentManager; + + @Reference + protected ManagedSymmetricEss ess; + + @Reference + protected ElectricityMeter meter; + + @Reference + protected Cycle cycle; + + protected LevlControlRequest currentRequest; + protected LevlControlRequest nextRequest; + + 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()); + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "ess", config.ess_id())) { + return; + } + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "meter", config.meter_id())) { + return; + } + + this.initChannelValues(); + } + + @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 = this.calculateRequiredPower(); + + /* + * set result + */ + this.ess.setActivePowerEqualsWithPid(calculatedPower); + this.ess.setReactivePowerEquals(0); + } + + 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); + this._setEssEfficiency(100.0); + this._setSocLowerBoundLevl(0.0); + this._setSocUpperBoundLevl(0.0); + this._setRemainingLevlEnergy(0L); + this._setBuyFromGridLimit(0L); + this._setSellToGridLimit(0L); + this._setInfluenceSellToGrid(false); + } + + /** + * Calculates the required charge/discharge power based on primary use case + * (puc; self consumption optimization) and levl. + * + * @return the required power for the next cycle [W] + * @throws OpenemsNamedException on error + */ + protected int calculateRequiredPower() throws OpenemsNamedException { + var cycleTimeS = this.cycle.getCycleTime() / MILLISECONDS_PER_SECOND; + + // load physical values + var physicalSoc = this.ess.getSoc().getOrError(); + 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.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 * 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 - round(Efficiency.apply(pucBatteryPower, efficiency) * cycleTimeS); + + // levl calculation + var levlBatteryPower = 0; + if (remainingLevlEnergyWs != 0) { + levlBatteryPower = this.calculateLevlBatteryPower(remainingLevlEnergyWs, pucBatteryPower, minEssPower, + maxEssPower, pucGridPower, buyFromGridLimit, sellToGridLimit, nextPucSocWs, levlSocWs, + socLowerBoundLevlPercent, socUpperBoundLevlPercent, essCapacityWs, influenceSellToGrid, efficiency, + cycleTimeS); + } + + // overall calculation + this._setPucBatteryPower(Long.valueOf(pucBatteryPower)); + var batteryPower = pucBatteryPower + levlBatteryPower; + return batteryPower; + } + + /** + * 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, 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; + } + + /** + * 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 ess capacity [Ws] + * @param minEssPower the minimum possible power of the ess [W] + * @param maxEssPower the maximum possible power of the ess [W] + * @param efficiency the efficiency of the system [%] + * @param cycleTimeS the configured openems cycle time [seconds] + * + * @return the puc battery power for the next cycle [W] + */ + protected int calculatePucBatteryPower(int gridPower, int essPower, long pucSocWs, long essCapacityWs, + int minEssPower, int maxEssPower, double efficiency, double cycleTimeS) { + this.log.debug("### calculatePucBatteryPower ###"); + this.log.debug(String.format( + "Parameters: gridPower=%d, essPower=%d, pucSocWs=%d, essCapacityWs=%d, minEssPower=%d, " + + "maxEssPower=%d, efficiency=%.2f, cycleTimeS=%.2f", + gridPower, essPower, pucSocWs, essCapacityWs, minEssPower, maxEssPower, efficiency, cycleTimeS)); + // calculate pucPower without any limits + var pucBatteryPower = gridPower + essPower; + this.log.debug("pucBatteryPower without limits: " + pucBatteryPower); + + // apply ess power limits + pucBatteryPower = TypeUtils.fitWithin(minEssPower, maxEssPower, pucBatteryPower); + this.log.debug("pucBatteryPower with ess power limits: " + pucBatteryPower); + + // apply soc bounds + pucBatteryPower = this.applyPucSocBounds(pucBatteryPower, pucSocWs, essCapacityWs, efficiency, cycleTimeS); + this.log.debug("pucBatteryPower with ess power and soc limits: " + pucBatteryPower); + return pucBatteryPower; + } + + /** + * Checks and corrects the puc battery power if it would exceed the upper or + * lower limits of the soc. + * + * @param pucPower the calculated pucPower [W] + * @param pucSocWs the soc of the puc [Ws] + * @param essCapacityWs the ess capacity [Ws] + * @param efficiency the efficiency of the system [%] + * @param cycleTimeS the configured openems cycle time [seconds] + * @return the restricted pucPower [W] + */ + protected int applyPucSocBounds(int pucPower, long pucSocWs, long essCapacityWs, double efficiency, + double cycleTimeS) { + var dischargeEnergyLowerBoundWs = pucSocWs - essCapacityWs; + var dischargeEnergyUpperBoundWs = pucSocWs; + + var powerLowerBound = Efficiency.unapply(round(dischargeEnergyLowerBoundWs / cycleTimeS), efficiency); + var powerUpperBound = Efficiency.unapply(round(dischargeEnergyUpperBoundWs / cycleTimeS), efficiency); + + if (powerLowerBound > 0) { + powerLowerBound = 0; + } + if (powerUpperBound < 0) { + powerUpperBound = 0; + } + + return (int) TypeUtils.fitWithin(powerLowerBound, powerUpperBound, pucPower); + } + + /** + * Calculates the battery power for the levl use case considering various + * constraints. + *

+ * positive = discharge; negative = charge + *

+ * + * @param remainingLevlEnergyWs the remaining energy that has to be realized + * for levl [Ws] + * @param pucBatteryPower the puc battery power [W] + * @param minEssPower the minimum possible power of the ess [W] + * @param maxEssPower the maximum possible power of the ess [W] + * @param pucGridPower the active power of the puc on the meter [W] + * @param buyFromGridLimit maximum power that may be bought from the + * grid [W] + * @param sellToGridLimit maximum power that may be sold to the grid + * [W] + * @param nextPucSocWs the calculated puc soc for the next cycle + * [Ws] + * @param levlSocWs the current levl soc [Ws] + * @param socLowerBoundLevlPercent the lower levl soc limit [%] + * @param socUpperBoundLevlPercent the upper levl soc limit [%] + * @param essCapacityWs the ess capacity [Ws] + * @param influenceSellToGrid whether it's allowed to influence sell to + * grid + * @param efficiency the efficiency of the system [%] + * @param cycleTimeS the configured openems cycle time [seconds] + * @return the levl battery power [W] + */ + protected int calculateLevlBatteryPower(long remainingLevlEnergyWs, int pucBatteryPower, int minEssPower, + int maxEssPower, int pucGridPower, long buyFromGridLimit, long sellToGridLimit, long nextPucSocWs, + long levlSocWs, double socLowerBoundLevlPercent, double socUpperBoundLevlPercent, long essCapacityWs, + boolean influenceSellToGrid, double efficiency, double cycleTimeS) { + + this.log.debug("### calculateLevlBatteryPowerW ###"); + this.log.debug(String.format( + "Parameters: remainingLevlEnergyWs=%d, pucBatteryPower=%d, minEssPower=%d, maxEssPower=%d, " + + "pucGridPower=%d, buyFromGridLimit=%d, sellToGridLimit=%d, nextPucSocWs=%d, levlSocWs=%d, " + + "socLowerBoundLevlPercent=%.2f, socUpperBoundLevlPercent=%.2f, essCapacityWs=%d, " + + "influenceSellToGrid=%b, efficiency=%.2f, cycleTimeS=%.2f", + remainingLevlEnergyWs, pucBatteryPower, minEssPower, maxEssPower, pucGridPower, buyFromGridLimit, + sellToGridLimit, nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, socUpperBoundLevlPercent, + essCapacityWs, influenceSellToGrid, efficiency, cycleTimeS)); + + var levlPower = round(remainingLevlEnergyWs / (double) cycleTimeS); + this.log.debug("Initial levlPower: " + levlPower); + + levlPower = this.applyBatteryPowerLimitsToLevlPower(levlPower, pucBatteryPower, minEssPower, maxEssPower); + this.log.debug("LevlPower after applyBatteryPowerLimits: " + levlPower); + + levlPower = this.applySocBoundariesToLevlPower(levlPower, nextPucSocWs, levlSocWs, socLowerBoundLevlPercent, + socUpperBoundLevlPercent, essCapacityWs, efficiency, cycleTimeS); + this.log.debug("LevlPower after applySocBoundaries: " + levlPower); + + levlPower = this.applyGridPowerLimitsToLevlPower(levlPower, pucGridPower, buyFromGridLimit, sellToGridLimit); + this.log.debug("LevlPower after applyGridPowerLimits: " + levlPower); + + levlPower = this.applyInfluenceSellToGridConstraint(levlPower, pucGridPower, influenceSellToGrid); + this.log.debug("LevlPower after applyInfluenceSellToGridConstraint: " + levlPower); + + return (int) levlPower; + } + + /** + * Applies battery power limits to the levl power. + * + * @param levlPower the levl battery power [W] + * @param pucBatteryPower the puc battery power [W] + * @param minEssPower the minimum possible power of the ess [W] + * @param maxEssPower the maximum possible power of the ess [W] + * @return the restricted levl battery power [W] + */ + protected long applyBatteryPowerLimitsToLevlPower(long levlPower, int pucBatteryPower, int minEssPower, + int maxEssPower) { + var levlPowerLowerBound = Long.valueOf(minEssPower) - pucBatteryPower; + var levlPowerUpperBound = Long.valueOf(maxEssPower) - pucBatteryPower; + return TypeUtils.fitWithin(levlPowerLowerBound, levlPowerUpperBound, levlPower); + } + + /** + * Applies upper and lower soc bounderies to the levl power. + * + * @param levlPower the levl battery power [W] + * @param nextPucSocWs the calculated puc soc for the next cycle + * [Ws] + * @param levlSocWs the current levl soc [Ws] + * @param socLowerBoundLevlPercent the lower levl soc limit [%] + * @param socUpperBoundLevlPercent the upper levl soc limit [%] + * @param essCapacityWs the ess capacity [Ws] + * @param efficiency the efficiency of the system [%] + * @param cycleTimeS the configured openems cycle time [seconds] + * @return the restricted levl battery power [W] + */ + protected long applySocBoundariesToLevlPower(long levlPower, long nextPucSocWs, long levlSocWs, + double socLowerBoundLevlPercent, double socUpperBoundLevlPercent, long essCapacityWs, double efficiency, + double cycleTimeS) { + var levlSocLowerBoundWs = round(socLowerBoundLevlPercent / HUNDRED_PERCENT * essCapacityWs) - nextPucSocWs; + var levlSocUpperBoundWs = round(socUpperBoundLevlPercent / HUNDRED_PERCENT * essCapacityWs) - nextPucSocWs; + + if (levlSocLowerBoundWs > 0) { + levlSocLowerBoundWs = 0; + } + if (levlSocUpperBoundWs < 0) { + levlSocUpperBoundWs = 0; + } + + var levlDischargeEnergyLowerBoundWs = -(levlSocUpperBoundWs - levlSocWs); + var levlDischargeEnergyUpperBoundWs = -(levlSocLowerBoundWs - levlSocWs); + + var levlPowerLowerBound = Efficiency.unapply(round(levlDischargeEnergyLowerBoundWs / cycleTimeS), efficiency); + var levlPowerUpperBound = Efficiency.unapply(round(levlDischargeEnergyUpperBoundWs / cycleTimeS), efficiency); + + return TypeUtils.fitWithin(levlPowerLowerBound, levlPowerUpperBound, levlPower); + } + + /** + * Applies grid power limits to the levl power. + * + * @param levlPower the levl battery power [W] + * @param pucGridPower the active power of the puc on the meter [W] + * @param buyFromGridLimit maximum power that may be bought from the grid [W] + * @param sellToGridLimit maximum power that may be sold to the grid [W] + * @return the restricted levl battery power [W] + */ + protected long applyGridPowerLimitsToLevlPower(long levlPower, int pucGridPower, long buyFromGridLimit, + long sellToGridLimit) { + var levlPowerLowerBound = -(buyFromGridLimit - pucGridPower); + var levlPowerUpperBound = -(sellToGridLimit - pucGridPower); + return TypeUtils.fitWithin(levlPowerLowerBound, levlPowerUpperBound, levlPower); + } + + /** + * Applies influence sell to grid constraint to the levl power. + * + * @param levlPower the levl battery power [W] + * @param pucGridPower the active power of the puc on the meter [W] + * @param influenceSellToGrid whether it's allowed to influence sell to grid + * @return the restricted levl battery power [W] + */ + protected long applyInfluenceSellToGridConstraint(long levlPower, int pucGridPower, boolean influenceSellToGrid) { + if (!influenceSellToGrid) { + if (pucGridPower < 0) { + // if primary use case sells to grid, levl isn't allowed to do anything + levlPower = 0; + } else { + // if primary use case buys from grid, levl can sell maximum this amount to grid + levlPower = min(levlPower, pucGridPower); + } + } + return levlPower; + } + + @Override + public void buildJsonApiRoutes(JsonApiBuilder builder) { + builder.handleRequest(METHOD, call -> { + return this.handleRequest(call); + }); + } + + /** + * Handles an incoming levl request. Updates the levl soc based on the request + * levl soc. + * + * @param call the JSON-RPC call + * @return a JSON-RPC response + * @throws OpenemsNamedException on error + */ + protected JsonrpcResponse handleRequest(Call call) throws OpenemsNamedException { + 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(); + 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)); + } + + 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 boolean isActive(LevlControlRequest request) { + var now = Instant.now(this.componentManager.getClock()); + return !(request == null || now.isBefore(request.start) || now.isAfter(request.deadline)); + } + + @Override + public void handleEvent(Event event) { + switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE -> { + if (this.isActive(this.nextRequest)) { + if (this.currentRequest != null) { + this.finishRequest(); + } + this.startNextRequest(); + } else if (this.currentRequest != null && !this.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); + } + } + } + } + + /** + * 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 { + var remainingLevlEnergy = this.getRemainingLevlEnergy().getOrError(); + + if (remainingLevlEnergy != 0L) { + var pucBatteryPower = this.getPucBatteryPowerChannel().getNextValue().getOrError(); + var levlPower = 0L; + var essNextPower = this.ess.getDebugSetActivePowerChannel().getNextValue(); + if (essNextPower.isDefined()) { + var essPower = essNextPower.get(); + levlPower = essPower - pucBatteryPower; + } + + var levlEnergyWs = round(levlPower * (this.cycle.getCycleTime() / MILLISECONDS_PER_SECOND)); + + // remaining for the next cycle + var newRemainingLevlEnergy = remainingLevlEnergy - levlEnergyWs; + if (this.hasSignChanged(remainingLevlEnergy, newRemainingLevlEnergy)) { + newRemainingLevlEnergy = 0; + } + this._setRemainingLevlEnergy(newRemainingLevlEnergy); + + // realized after the next cycle + var realizedEnergyGridWs = this.getRealizedEnergyGrid().getOrError(); + this._setRealizedEnergyGrid(realizedEnergyGridWs + levlEnergyWs); + + // realized after the next cycle + var efficiency = this.getEssEfficiency().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. + */ + 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.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._setRemainingLevlEnergy(0L); + this._setRealizedEnergyGrid(0L); + this._setRealizedEnergyBattery(0L); + 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; + this.nextRequest = null; + this._setEssEfficiency(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); + } + + 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 new file mode 100644 index 00000000000..1f8d164e910 --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/LevlControlRequest.java @@ -0,0 +1,139 @@ +package io.openems.edge.levl.controller; + +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 io.openems.common.utils.JsonUtils; +import java.time.Instant; +import java.util.Objects; + +public class LevlControlRequest { + public static final String METHOD = "sendLevlControlRequest"; + public static final int QUARTER_HOUR_SECONDS = 900; + protected int sellToGridLimitW; + protected int buyFromGridLimitW; + protected String levlRequestId; + protected String timestamp; + protected long energyWs; + protected Instant start; + protected Instant deadline; + protected int levlSocWh; + protected double socLowerBoundPercent; + protected double socUpperBoundPercent; + protected double efficiencyPercent; + protected boolean influenceSellToGrid; + + + protected LevlControlRequest(JsonObject params, Instant now) throws OpenemsError.OpenemsNamedException { + try { + this.parseFields(params, now); + } 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 + protected LevlControlRequest() { + + } + + //Just for testing + protected LevlControlRequest(int sellToGridLimitW, int buyFromGridLimitW, String levlRequestId, String timestamp, + long energyWs, Instant start, Instant 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; + } + + //Just for testing + protected LevlControlRequest(int startDelay, int duration, Instant now) { + this.start = now.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 + * @param now the current time + * @return the levl control request + * @throws OpenemsNamedException on error + */ + protected static LevlControlRequest from(JsonrpcRequest request, Instant now) throws OpenemsNamedException { + var params = request.getParams(); + return new LevlControlRequest(params, now); + } + + 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 = 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"); + 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 + 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); + } + + @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 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=" + 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/common/Efficiency.java b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java new file mode 100644 index 00000000000..7c79081a842 --- /dev/null +++ b/io.openems.edge.levl.controller/src/io/openems/edge/levl/controller/common/Efficiency.java @@ -0,0 +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 + *

+ * + * @param value power/energy to which the efficiency should be + * applied + * @param efficiencyPercent efficiency which should be applied + * @return the power/energy inside the battery after applying the efficiency + */ + public static long apply(long value, double efficiencyPercent) { + if (value <= 0) { // charge + return multiplyByEfficiency(value, efficiencyPercent); + } + + // discharge + return divideByEfficiency(value, efficiencyPercent); + } + + /** + * Unapplies an efficiency to a power/energy inside of the battery. + * + *

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

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