From ab3cc8edf552d135e8ed5ba2533584fbf2b35e6d Mon Sep 17 00:00:00 2001 From: sebastianasen <47855186+sebastianasen@users.noreply.github.com> Date: Thu, 19 Nov 2020 22:31:59 +0100 Subject: [PATCH] Further development and rework of the EVCS (Electric Vehicle Charging Station) components (#1263) * Changed small time issues in the keba implementation. Added unknown state in the UI. * Added EvcsPower and therefore a new Filter "RampFilter". Because of that, every ManagedEvcs impl. needs a the current reference of the EvcsPowerComponent, to have the global ramp filter and in the future other things of the EvcsPower Interface. Initial rework of the EvcsCluster. Small format changes. * Wrote new test for the EVCS controller. * changed a testcase in the EvcsController test, to check the changes in the config instead of checking the _Property Channels. * Evcs Cluster: Created a new unit test and changed a few things in the Evcs Cluster components. * EvcsController: Changed code for the disabled charging mode and adapted the tests * EvcsCluster: Changed code for the minimumGuaranteed power that a charging station needs and adapted the tests * Changed the hadling of the maximum Power of an EVCS. It will be used that value of the maximumum for calculation, but the sent charge power will be like before. * General changes for the EnergySession calculation if there's only a total energy value. Adjustments to the ABL implementation * Send also a chargePowerRequest of 0 if charging is disabled * Added logic, to reduce the value calculated from the "maximum allowed discharge power" of the ess, to avoid certain jumps to zero, if the essSoc gets lower. * Added a total energy value in the EVCS Nature (ACTIVE_CONSUMPTION_ENERGY). Set the total energy and session energy depending on one energy value form the chargingstation (depending on the chargingstation we get the total or the session energy). * Rework of the energy calculation. Co-authored-by: Stefan Feilmeier --- io.openems.edge.application/EdgeApp.bndrun | 2 + .../common/filter}/DisabledPidFilter.java | 9 +- .../common/filter/DisabledRampFilter.java | 15 + .../edge/common/filter/RampFilter.java | 110 +++ .../evcs/ChargingLowerThanTargetHandler.java | 43 +- .../openems/edge/controller/evcs/Config.java | 3 + .../edge/controller/evcs/EvcsController.java | 388 +++++++++- .../controller/evcs/EvcsControllerImpl.java | 343 --------- .../evcs/EvcsControllerImplTest.java | 128 ---- .../controller/evcs/EvcsControllerTest.java | 313 ++++++++ .../edge/controller/evcs/MyConfig.java | 74 +- .../ess/core/power/PowerComponentImpl.java | 1 + .../src/io/openems/edge/evcs/api/Evcs.java | 53 ++ .../io/openems/edge/evcs/api/EvcsPower.java | 13 + .../io/openems/edge/evcs/api/ManagedEvcs.java | 87 ++- .../openems/edge/evcs/api/MeasuringEvcs.java | 7 +- .../edge/evcs/test/DummyEvcsPower.java | 23 + .../edge/evcs/test/DummyManagedEvcs.java | 11 +- .../evcs/cluster/AbstractEvcsCluster.java | 120 ++- .../edge/evcs/cluster/ConfigPeakShaving.java | 11 +- .../evcs/cluster/EvcsClusterPeakShaving.java | 81 ++- .../cluster/EvcsClusterSelfConsumption.java | 3 +- .../cluster/EvcsClusterPeakShavingTest.java | 108 --- .../edge/evcs/cluster/EvcsClusterTest.java | 687 ++++++++++++++++++ .../evcs/cluster/MyConfigPeakShaving.java | 110 ++- io.openems.edge.evcs.core/.classpath | 12 + io.openems.edge.evcs.core/.gitignore | 2 + io.openems.edge.evcs.core/.project | 23 + io.openems.edge.evcs.core/bnd.bnd | 14 + io.openems.edge.evcs.core/readme.adoc | 5 + .../src/io/openems/edge/evcs/core/Config.java | 21 + .../edge/evcs/core/EvcsPowerComponent.java | 73 ++ io.openems.edge.evcs.core/test/.gitignore | 0 .../evcs/keba/kecontact/KebaKeContact.java | 11 +- .../edge/evcs/keba/kecontact/ReadHandler.java | 24 +- .../edge/evcs/keba/kecontact/ReadWorker.java | 7 +- io.openems.edge.evcs.ocpp.abl/bnd.bnd | 1 + .../io/openems/edge/evcs/ocpp/abl/Abl.java | 57 +- .../io/openems/edge/evcs/ocpp/abl/Config.java | 8 +- io.openems.edge.evcs.ocpp.common/bnd.bnd | 3 +- .../common/AbstractOcppEvcsComponent.java | 105 ++- .../evcs/ocpp/common/ChargeSessionStamp.java | 82 +++ .../bnd.bnd | 3 +- .../ocpp/ies/keywatt/singleccs/Config.java | 2 +- .../singleccs/IesKeywattSingleCcs.java | 20 +- io.openems.edge.evcs.ocpp.server/bnd.bnd | 1 + .../ocpp/server/CoreEventHandlerImpl.java | 71 +- .../edge/evcs/ocpp/server/OcppServerImpl.java | 23 +- .../openems/edge/simulator/evcs/Config.java | 6 - .../edge/simulator/evcs/SimulatedEvcs.java | 60 +- 50 files changed, 2517 insertions(+), 860 deletions(-) rename {io.openems.edge.ess.core/src/io/openems/edge/ess/core/power => io.openems.edge.common/src/io/openems/edge/common/filter}/DisabledPidFilter.java (53%) create mode 100644 io.openems.edge.common/src/io/openems/edge/common/filter/DisabledRampFilter.java create mode 100644 io.openems.edge.common/src/io/openems/edge/common/filter/RampFilter.java delete mode 100644 io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/EvcsControllerImpl.java delete mode 100644 io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/EvcsControllerImplTest.java create mode 100644 io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/EvcsControllerTest.java create mode 100644 io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/EvcsPower.java create mode 100644 io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyEvcsPower.java delete mode 100644 io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterPeakShavingTest.java create mode 100644 io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterTest.java create mode 100644 io.openems.edge.evcs.core/.classpath create mode 100644 io.openems.edge.evcs.core/.gitignore create mode 100644 io.openems.edge.evcs.core/.project create mode 100644 io.openems.edge.evcs.core/bnd.bnd create mode 100644 io.openems.edge.evcs.core/readme.adoc create mode 100644 io.openems.edge.evcs.core/src/io/openems/edge/evcs/core/Config.java create mode 100644 io.openems.edge.evcs.core/src/io/openems/edge/evcs/core/EvcsPowerComponent.java create mode 100644 io.openems.edge.evcs.core/test/.gitignore create mode 100644 io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/ChargeSessionStamp.java diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index d2b4a3ce080..12b70031f8e 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -96,6 +96,7 @@ bnd.identity;id='io.openems.edge.ess.sma',\ bnd.identity;id='io.openems.edge.ess.streetscooter',\ bnd.identity;id='io.openems.edge.evcs.cluster',\ + bnd.identity;id='io.openems.edge.evcs.core',\ bnd.identity;id='io.openems.edge.evcs.keba.kecontact',\ bnd.identity;id='io.openems.edge.evcs.ocpp.abl',\ bnd.identity;id='io.openems.edge.evcs.ocpp.common',\ @@ -223,6 +224,7 @@ io.openems.edge.ess.streetscooter;version=snapshot,\ io.openems.edge.evcs.api;version=snapshot,\ io.openems.edge.evcs.cluster;version=snapshot,\ + io.openems.edge.evcs.core;version=snapshot,\ io.openems.edge.evcs.keba.kecontact;version=snapshot,\ io.openems.edge.evcs.ocpp.abl;version=snapshot,\ io.openems.edge.evcs.ocpp.common;version=snapshot,\ diff --git a/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/DisabledPidFilter.java b/io.openems.edge.common/src/io/openems/edge/common/filter/DisabledPidFilter.java similarity index 53% rename from io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/DisabledPidFilter.java rename to io.openems.edge.common/src/io/openems/edge/common/filter/DisabledPidFilter.java index b248df6759d..902f5040456 100644 --- a/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/DisabledPidFilter.java +++ b/io.openems.edge.common/src/io/openems/edge/common/filter/DisabledPidFilter.java @@ -1,11 +1,10 @@ -package io.openems.edge.ess.core.power; - -import io.openems.edge.common.filter.PidFilter; +package io.openems.edge.common.filter; /** * This implementation ignores the PID filter and instead just returns the - * unfiltered target value. It is used when {@link PowerComponent} is configured - * to disable PID filter. + * unfiltered target value - making sure it is within the allowed minimum and + * maximum limits. It is used when {@link PowerComponent} is configured to + * disable PID filter. */ public class DisabledPidFilter extends PidFilter { diff --git a/io.openems.edge.common/src/io/openems/edge/common/filter/DisabledRampFilter.java b/io.openems.edge.common/src/io/openems/edge/common/filter/DisabledRampFilter.java new file mode 100644 index 00000000000..d5a1c56644e --- /dev/null +++ b/io.openems.edge.common/src/io/openems/edge/common/filter/DisabledRampFilter.java @@ -0,0 +1,15 @@ +package io.openems.edge.common.filter; + +/** + * This implementation ignores the Ramp filter and instead just returns the + * unfiltered target value - making sure it is within the allowed minimum and + * maximum limits. It is used when the using Component is configured to disable + * Ramp filter. + */ +public class DisabledRampFilter extends RampFilter { + + @Override + public int applyRampFilter(int input, int target) { + return this.applyLowHighLimits(target); + } +} diff --git a/io.openems.edge.common/src/io/openems/edge/common/filter/RampFilter.java b/io.openems.edge.common/src/io/openems/edge/common/filter/RampFilter.java new file mode 100644 index 00000000000..417d252c689 --- /dev/null +++ b/io.openems.edge.common/src/io/openems/edge/common/filter/RampFilter.java @@ -0,0 +1,110 @@ +package io.openems.edge.common.filter; + +import io.openems.common.exceptions.OpenemsException; + +/** + * A controller that increases the input by a given increase rate. + */ +public class RampFilter { + + public static final double DEFAULT_INCREASE_RATE = 0.05; + + private double increasingRate; + + private Integer lowLimit = null; + private Integer highLimit = null; + + /** + * Creates a RampFilter + * + * @param increasingRate the rate of increase + */ + public RampFilter(double increasingRate) { + this.increasingRate = DEFAULT_INCREASE_RATE; + if (increasingRate > 0 && increasingRate < 1) { + this.increasingRate = increasingRate; + } + } + + /** + * Creates a RampFilter using default values + */ + public RampFilter() { + this(DEFAULT_INCREASE_RATE); + } + + /** + * Limit the output value. + * + * @param lowLimit lowest allowed output value + * @param highLimit highest allowed output value + */ + public void setLimits(Integer lowLimit, Integer highLimit) { + if (lowLimit != null && highLimit != null && lowLimit > highLimit) { + throw new IllegalArgumentException( + "Given LowLimit [" + lowLimit + "] is higher than HighLimit [" + highLimit + "]"); + } + this.lowLimit = lowLimit; + this.highLimit = highLimit; + } + + /** + * Apply the filter using the current Channel value as input and the target + * value. + * + * @param input the input value, e.g. the measured Channel value + * @param target the target value + * @return the filtered set-point value + * @throws OpenemsException + */ + public int applyRampFilter(int input, int target) throws OpenemsException { + + // Pre-process the target value: apply output value limits + target = this.applyLowHighLimits(target); + + // Make sure that there is a highLimit set + if (this.highLimit == null) { + throw new OpenemsException( + "No high limit given in EvcsFilter. Please call setLimits bevore applying the filter."); + } + + // We are already there + if (input == target) { + return target; + } + + // Calculate the next additional power + double additionalPower = this.highLimit * this.increasingRate; + + // Next power + double output = input; + if (input < target) { + // input should increase + output += additionalPower; + output = output > target ? target : output; + } else { + // input should decrease + output -= additionalPower; + output = output < target ? target : output; + } + + // Post-process the output value: convert to integer + return Math.round((float) output); + } + + /** + * Applies the configured PID low and high limits to a value. + * + * @param value the input value + * @return the value within low and high limit + */ + protected int applyLowHighLimits(int value) { + if (this.lowLimit != null && value < this.lowLimit) { + value = this.lowLimit; + } + if (this.highLimit != null && value > this.highLimit) { + value = this.highLimit; + } + return value; + } +} diff --git a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ChargingLowerThanTargetHandler.java b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ChargingLowerThanTargetHandler.java index f840f9b0015..3f27eee1653 100644 --- a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ChargingLowerThanTargetHandler.java +++ b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/ChargingLowerThanTargetHandler.java @@ -15,13 +15,14 @@ public class ChargingLowerThanTargetHandler { */ private static final int MAXIMUM_OUT_OF_RANGE_TRIES = 3; private int outOfRangeCounter = 0; - private static final double CHARGING_TARGET_MAX_DIFFERENCE_PERCENT = 0.10; // 10% - private static final int CHECK_CHARGING_TARGET_DIFFERENCE_TIME = 30; // sec + private static final double CHARGING_TARGET_MAX_DIFFERENCE_PERCENT = 0.15; // 10% + private static final int CHECK_CHARGING_TARGET_DIFFERENCE_TIME = 45; // sec private LocalDateTime lastChargingCheck = LocalDateTime.now(); + private Integer maximumChargePower = null; // W - private final EvcsControllerImpl parent; + private final EvcsController parent; - public ChargingLowerThanTargetHandler(EvcsControllerImpl parent) { + public ChargingLowerThanTargetHandler(EvcsController parent) { this.parent = parent; } @@ -53,20 +54,46 @@ protected boolean isLower(ManagedEvcs evcs) throws InvalidValueException { /** * Check if the difference between the requested charging target and the real - * charging power is higher than the CHARGING_TARGET_MAX_DIFFERENCE. + * charging power is higher than the CHARGING_TARGET_MAX_DIFFERENCE. If it + * returns true, it is setting the maximumPower. * * @param evcs EVCS * @return true if the difference is too high * @throws InvalidValueException invalidValueException */ protected boolean isChargingLowerThanTarget(ManagedEvcs evcs) throws InvalidValueException { - int chargingPower = evcs.getChargePower().orElse(0); - int chargingPowerTarget = evcs.getSetChargePowerLimit().orElse(evcs.getMaximumHardwarePower().getOrError()); - if (chargingPowerTarget - chargingPower > chargingPowerTarget * CHARGING_TARGET_MAX_DIFFERENCE_PERCENT) { + int chargePower = evcs.getChargePower().orElse(0); + int chargePowerTarget = evcs.getSetChargePowerLimit().orElse(evcs.getMaximumHardwarePower().getOrError()); + + if (chargePowerTarget - chargePower > chargePowerTarget * CHARGING_TARGET_MAX_DIFFERENCE_PERCENT) { + this.maximumChargePower = calculateMaximumPower(chargePower); this.lastChargingCheck = LocalDateTime.now(); return true; } + this.maximumChargePower = null; return false; } + /** + * Returns the calculated maximum charge power depending on the current charge + * power and the current maximum charge power. + * + * @param chargingPower current charge power + * @return the current charge power or one of the past charge power values + */ + private Integer calculateMaximumPower(int currentChargePower) { + if (this.maximumChargePower == null) { + return currentChargePower; + } + return currentChargePower > this.maximumChargePower ? currentChargePower : this.maximumChargePower; + } + + /** + * Maximum charge power of the EV depending on the last {@link #isChargingLowerThanTarget(ManagedEvcs)} try's. + * + * @return The maximum charge power of the EV. + */ + protected Integer getMaximumChargePower() { + return this.maximumChargePower; + } } diff --git a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/Config.java b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/Config.java index 91650a71025..c138fb33585 100644 --- a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/Config.java +++ b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/Config.java @@ -43,6 +43,9 @@ @AttributeDefinition(name = "Energy limit in this session in [Wh]", description = "Set the Energylimit in this Session in Wh. The charging station will only charge till this limit; '0' is no limit.") int energySessionLimit() default 0; + + @AttributeDefinition(name = "Evcs target filter", description = "This is auto-generated by 'Evcs-ID'.") + String evcs_target() default ""; String webconsole_configurationFactory_nameHint() default "Controller Electric Vehicle Charging Station [{id}]"; diff --git a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/EvcsController.java b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/EvcsController.java index 308224624a7..265c2c26e89 100644 --- a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/EvcsController.java +++ b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/EvcsController.java @@ -1,39 +1,379 @@ package io.openems.edge.controller.evcs; -import io.openems.common.channel.Unit; -import io.openems.common.types.OpenemsType; -import io.openems.edge.common.channel.Doc; +import java.io.IOException; +import java.time.Clock; +import java.util.Dictionary; +import java.util.Optional; + +import org.osgi.service.cm.Configuration; +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.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.common.channel.AccessMode; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; +import io.openems.edge.common.channel.Channel; +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.modbusslave.ModbusSlave; +import io.openems.edge.common.modbusslave.ModbusSlaveTable; +import io.openems.edge.common.sum.Sum; import io.openems.edge.controller.api.Controller; +import io.openems.edge.ess.api.ManagedSymmetricEss; +import io.openems.edge.ess.api.SymmetricEss; +import io.openems.edge.ess.power.api.Phase; +import io.openems.edge.ess.power.api.Power; +import io.openems.edge.ess.power.api.Pwr; +import io.openems.edge.evcs.api.Evcs; +import io.openems.edge.evcs.api.ManagedEvcs; +import io.openems.edge.evcs.api.Status; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Controller.Evcs", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class EvcsController extends AbstractOpenemsComponent implements Controller, OpenemsComponent, ModbusSlave { + + private final Logger log = LoggerFactory.getLogger(EvcsController.class); + private static final int CHARGE_POWER_BUFFER = 200; + private static final double DEFAULT_UPPER_TARGET_DIFFERENCE_PERCENT = 0.10; // 10% + + private final ChargingLowerThanTargetHandler chargingLowerThanTargetHandler; -public interface EvcsController extends Controller, OpenemsComponent, ModbusSlave { + private Config config; - public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - CHARGE_MODE(Doc.of(ChargeMode.values()) // - .initialValue(ChargeMode.FORCE_CHARGE) // - .text("Configured Charge-Mode")), // - FORCE_CHARGE_MINPOWER(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT).text("Minimum value for the force charge per Phase")), - DEFAULT_CHARGE_MINPOWER(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT) // - .text("Minimum value for a default charge")), - PRIORITY(Doc.of(Priority.values()) // - .initialValue(Priority.CAR) // - .text("Which component getting preferred")), // - ENABLED_CHARGING(Doc.of(OpenemsType.BOOLEAN) // - .text("Activates or deactivates the Charging")); // + @Reference + protected ComponentManager componentManager; - private final Doc doc; + @Reference + protected ConfigurationAdmin cm; + + @Reference + protected Sum sum; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + private ManagedEvcs evcs; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + private SymmetricEss ess; + + public EvcsController() { + this(Clock.systemDefaultZone()); + } - private ChannelId(Doc doc) { - this.doc = doc; + protected EvcsController(Clock clock) { + super(// + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values() // + ); + this.chargingLowerThanTargetHandler = new ChargingLowerThanTargetHandler(this); + } + + @Activate + void activate(ComponentContext context, Config config) throws OpenemsNamedException { + super.activate(context, config.id(), config.alias(), config.enabled()); + + if (config.forceChargeMinPower() < 0) { + throw new OpenemsException("Force-Charge Min-Power [" + config.forceChargeMinPower() + "] must be >= 0"); } - @Override - public Doc doc() { - return this.doc; + if (config.defaultChargeMinPower() < 0) { + throw new OpenemsException( + "Default-Charge Min-Power [" + config.defaultChargeMinPower() + "] must be >= 0"); } + + this.config = config; + + if (OpenemsComponent.updateReferenceFilter(cm, this.servicePid(), "evcs", config.evcs_id())) { + return; + } + if (OpenemsComponent.updateReferenceFilter(cm, this.servicePid(), "ess", config.ess_id())) { + return; + } + this.evcs._setMaximumPower(null); + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); } + /** + * If the EVCS is clustered the method will set the charge power request. + * Otherwise it will set directly the charge power limit. + */ + @Override + public void run() throws OpenemsNamedException { + + boolean isClustered = this.evcs.getIsClustered().orElse(false); + + /* + * Stop early if charging is disabled + */ + if (!this.config.enabledCharging()) { + this.evcs.setChargePowerLimit(0); + if (isClustered) { + this.evcs.setChargePowerRequest(0); + this.resetMinMaxChannels(); + } + return; + } + + adaptConfigToHardwareLimits(); + + this.evcs.setEnergyLimit(this.config.energySessionLimit()); + + /* + * Sets a fixed request of 0 if the Charger is not ready + */ + if (isClustered) { + + Status status = this.evcs.getStatus(); + switch (status) { + case ERROR: + case STARTING: + case UNDEFINED: + case NOT_READY_FOR_CHARGING: + case ENERGY_LIMIT_REACHED: + this.evcs.setChargePowerRequest(0); + resetMinMaxChannels(); + return; + case CHARGING_REJECTED: + case READY_FOR_CHARGING: + case CHARGING_FINISHED: + this.evcs._setMaximumPower(null); + case CHARGING: + break; + } + } + + int nextChargePower = 0; + int nextMinPower = 0; + + /* + * Calculates the next charging power depending on the charge mode + */ + switch (config.chargeMode()) { + case EXCESS_POWER: + /* + * Get the next charge power depending on the priority. + */ + switch (config.priority()) { + case CAR: + nextChargePower = this.calculateChargePowerFromExcessPower(this.evcs); + break; + + case STORAGE: + int storageSoc = this.sum.getEssSoc().orElse(0); + if (storageSoc > 97) { + nextChargePower = this.calculateChargePowerFromExcessPower(this.evcs); + } else { + nextChargePower = this.calculateExcessPowerAfterEss(this.evcs, this.ess); + } + break; + } + + Channel minimumHardwarePowerChannel = evcs.channel(Evcs.ChannelId.MINIMUM_HARDWARE_POWER); + if (nextChargePower < minimumHardwarePowerChannel.value() + .orElse(0)) { /* charging under 6A isn't possible */ + nextChargePower = 0; + } + + this.evcs._setMinimumPower(config.defaultChargeMinPower()); + nextMinPower = config.defaultChargeMinPower(); + break; + + case FORCE_CHARGE: + this.evcs._setMinimumPower(0); + nextChargePower = config.forceChargeMinPower() * this.evcs.getPhases().orElse(3); + break; + } + + if (nextChargePower < nextMinPower) { + nextChargePower = nextMinPower; + } + + // charging under minimum hardware power isn't possible + Channel minimumHardwarePowerChannel = evcs.channel(Evcs.ChannelId.MINIMUM_HARDWARE_POWER); + if (nextChargePower < minimumHardwarePowerChannel.value().orElse(0)) { + nextChargePower = 0; + } + + /** + * Calculates the maximum Power of the Car. + */ + if (nextChargePower != 0) { + + int chargePower = this.evcs.getChargePower().orElse(0); + + /** + * Check the difference of the current charge power and the previous charging + * target + */ + if (this.chargingLowerThanTargetHandler.isLower(this.evcs)) { + + Integer maximumPower = this.chargingLowerThanTargetHandler.getMaximumChargePower(); + if (maximumPower != null) { + this.evcs._setMaximumPower(maximumPower + CHARGE_POWER_BUFFER); + this.logDebug(this.log, + "Maximum Charge Power of the EV reduced to" + maximumPower + " W plus buffer"); + } + } else { + int currMax = this.evcs.getMaximumPower().orElse(0); + + /** + * If the charge power would increases again above the current maximum power, it + * resets the maximum Power. + */ + if (chargePower > currMax * (1 + DEFAULT_UPPER_TARGET_DIFFERENCE_PERCENT)) { + this.evcs._setMaximumPower(null); + } + } + } + + if (isClustered) { + this.evcs.setChargePowerRequest(nextChargePower); + } else { + this.evcs.setChargePowerLimit(nextChargePower); + } + this.logDebug(this.log, "Next charge power: " + nextChargePower + " W"); + } + + /** + * Reseting the minimum and maximum power channels. + */ + private void resetMinMaxChannels() { + evcs._setMinimumPower(0); + evcs._setMaximumPower(null); + } + + /** + * Adapt the charge limits to the given hardware limits of the EVCS. + */ + private void adaptConfigToHardwareLimits() { + + Optional maxHardwareOpt = this.evcs.getMaximumHardwarePower().asOptional(); + if (maxHardwareOpt.isPresent()) { + int maxHW = maxHardwareOpt.get(); + if (maxHW != 0) { + maxHW = (int) Math.ceil(maxHW / 100.0) * 100; + if (config.defaultChargeMinPower() > maxHW) { + configUpdate("defaultChargeMinPower", maxHW); + } + } + } + + } + + /** + * Calculates the next charging power, depending on the current PV production + * and house consumption. + * + * @param evcs Electric Vehicle Charging Station + * @return the available excess power for charging + * @throws OpenemsNamedException on error + */ + private int calculateChargePowerFromExcessPower(ManagedEvcs evcs) throws OpenemsNamedException { + + int buyFromGrid = this.sum.getGridActivePower().orElse(0); + int essDischarge = this.sum.getEssActivePower().orElse(0); + int essActivePowerDC = this.sum.getProductionDcActualPower().orElse(0); + int evcsCharge = evcs.getChargePower().orElse(0); + + return evcsCharge - buyFromGrid - (essDischarge - essActivePowerDC); + } + + /** + * Calculates the next charging power from excess power after Ess charging. + * + * @param evcs the ManagedEvcs + * @param ess the ManagedSymmetricEss + * @return the available excess power for charging + */ + private int calculateExcessPowerAfterEss(ManagedEvcs evcs, SymmetricEss ess) { + int maxEssCharge; + if (ess instanceof ManagedSymmetricEss) { + ManagedSymmetricEss e = (ManagedSymmetricEss) ess; + Power power = ((ManagedSymmetricEss) ess).getPower(); + maxEssCharge = power.getMinPower(e, Phase.ALL, Pwr.ACTIVE); + maxEssCharge = Math.abs(maxEssCharge); + } else { + maxEssCharge = ess.getMaxApparentPower().orElse(0); + } + int buyFromGrid = this.sum.getGridActivePower().orElse(0); + int essActivePower = this.sum.getEssActivePower().orElse(0); + int essActivePowerDC = this.sum.getProductionDcActualPower().orElse(0); + int evcsCharge = evcs.getChargePower().orElse(0); + int result = -buyFromGrid + evcsCharge - (maxEssCharge + (essActivePower - essActivePowerDC)); + result = result > 0 ? result : 0; + + return result; + } + + @Override + public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { + return new ModbusSlaveTable(// + OpenemsComponent.getModbusSlaveNatureTable(accessMode), // + Controller.getModbusSlaveNatureTable(accessMode)); + } + + /** + * Updating the configuration property to given value. + * + * @param targetProperty Property that should be changed + * @param requiredValue Value that should be set + */ + public void configUpdate(String targetProperty, Object requiredValue) { + + Configuration c; + try { + String pid = this.servicePid(); + if (pid.isEmpty()) { + this.logInfo(log, "PID of " + this.id() + " is Empty"); + return; + } + c = cm.getConfiguration(pid, "?"); + Dictionary properties = c.getProperties(); + Object target = properties.get(targetProperty); + String existingTarget = target.toString(); + if (!existingTarget.isEmpty()) { + properties.put(targetProperty, requiredValue); + c.update(properties); + } + } catch (IOException | SecurityException e) { + this.logError(log, "ERROR: " + e.getMessage()); + } + } + + @Override + public void logInfo(Logger log, String message) { + super.logInfo(log, message); + } + + @Override + protected void logWarn(Logger log, String message) { + super.logWarn(log, message); + } + + @Override + protected void logDebug(Logger log, String message) { + if (this.config.debugMode()) { + this.logInfo(this.log, message); + } + } } diff --git a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/EvcsControllerImpl.java b/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/EvcsControllerImpl.java deleted file mode 100644 index 72fa4611b22..00000000000 --- a/io.openems.edge.controller.evcs/src/io/openems/edge/controller/evcs/EvcsControllerImpl.java +++ /dev/null @@ -1,343 +0,0 @@ -package io.openems.edge.controller.evcs; - -import java.io.IOException; -import java.util.Dictionary; - -import org.osgi.service.cm.Configuration; -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.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; -import org.osgi.service.component.annotations.ReferencePolicyOption; -import org.osgi.service.metatype.annotations.Designate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.openems.common.channel.AccessMode; -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; -import io.openems.edge.common.channel.Channel; -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.modbusslave.ModbusSlave; -import io.openems.edge.common.modbusslave.ModbusSlaveTable; -import io.openems.edge.common.sum.Sum; -import io.openems.edge.controller.api.Controller; -import io.openems.edge.ess.api.ManagedSymmetricEss; -import io.openems.edge.ess.api.SymmetricEss; -import io.openems.edge.ess.power.api.Phase; -import io.openems.edge.ess.power.api.Power; -import io.openems.edge.ess.power.api.Pwr; -import io.openems.edge.evcs.api.Evcs; -import io.openems.edge.evcs.api.ManagedEvcs; -import io.openems.edge.evcs.api.Status; - -@Designate(ocd = Config.class, factory = true) -@Component(// - name = "Controller.Evcs", // - immediate = true, // - configurationPolicy = ConfigurationPolicy.REQUIRE // -) -public class EvcsControllerImpl extends AbstractOpenemsComponent - implements EvcsController, Controller, OpenemsComponent, ModbusSlave { - - private final Logger log = LoggerFactory.getLogger(EvcsControllerImpl.class); - private static final int CHARGE_POWER_BUFFER = 100; - private static final double DEFAULT_UPPER_TARGET_DIFFERENCE_PERCENT = 0.10; // 10% - - private final ChargingLowerThanTargetHandler chargingLowerThanTargetHandler; - - private Config config; - - @Reference - protected ComponentManager componentManager; - - @Reference - protected ConfigurationAdmin cm; - - @Reference - protected Sum sum; - - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) - private ManagedEvcs evcs; - - public EvcsControllerImpl() { - super(// - OpenemsComponent.ChannelId.values(), // - Controller.ChannelId.values(), // - EvcsController.ChannelId.values() // - ); - this.chargingLowerThanTargetHandler = new ChargingLowerThanTargetHandler(this); - } - - @Activate - void activate(ComponentContext context, Config config) throws OpenemsNamedException { - super.activate(context, config.id(), config.alias(), config.enabled()); - - if (config.forceChargeMinPower() < 0) { - throw new OpenemsException("Force-Charge Min-Power [" + config.forceChargeMinPower() + "] must be >= 0"); - } - - if (config.defaultChargeMinPower() < 0) { - throw new OpenemsException( - "Default-Charge Min-Power [" + config.defaultChargeMinPower() + "] must be >= 0"); - } - - this.config = config; - - this.channel(EvcsController.ChannelId.CHARGE_MODE).setNextValue(config.chargeMode()); - this.channel(EvcsController.ChannelId.PRIORITY).setNextValue(config.priority()); - this.channel(EvcsController.ChannelId.ENABLED_CHARGING).setNextValue(config.enabledCharging()); - this.channel(EvcsController.ChannelId.DEFAULT_CHARGE_MINPOWER).setNextValue(config.defaultChargeMinPower()); - this.channel(EvcsController.ChannelId.FORCE_CHARGE_MINPOWER).setNextValue(config.forceChargeMinPower()); - - if (OpenemsComponent.updateReferenceFilter(cm, this.servicePid(), "evcs", config.evcs_id())) { - return; - } - this.evcs._setMaximumPower(evcs.getMaximumHardwarePower().orElse(22800)); - } - - @Override - @Deactivate - protected void deactivate() { - super.deactivate(); - } - - /** - * If the EVCS is clustered the method will set the charge power request. - * Otherwise it will set directly the charge power limit. - */ - @Override - public void run() throws OpenemsNamedException { - SymmetricEss ess = this.componentManager.getComponent(config.ess_id()); - int maxHW = evcs.getMaximumHardwarePower().orElse(22800); - if (maxHW != 0) { - maxHW = (int) Math.ceil(maxHW / 100.0) * 100; - if (config.defaultChargeMinPower() > maxHW) { - configUpdate("defaultChargeMinPower", maxHW); - } - if (config.forceChargeMinPower() * evcs.getPhases().orElse(3) > maxHW) { - configUpdate("forceChargeMinPower", maxHW / 3); - } - } - - evcs.setEnergyLimit(config.energySessionLimit()); - - /* - * Sets a fixed request of 0 if the Charger is not ready - */ - boolean isClustered = evcs.getIsClustered().orElse(false); - if (isClustered) { - - Status status = evcs.getStatus(); - if (status == null) { - evcs._setStatus(Status.NOT_READY_FOR_CHARGING); - status = Status.NOT_READY_FOR_CHARGING; - } - switch (status) { - case ERROR: - case STARTING: - case UNDEFINED: - case NOT_READY_FOR_CHARGING: - case ENERGY_LIMIT_REACHED: - evcs.setChargePowerRequest(0); - evcs._setMinimumPower(0); - evcs._setMaximumPower(null); - return; - case CHARGING_REJECTED: - case READY_FOR_CHARGING: - case CHARGING_FINISHED: - evcs._setMaximumPower(null); - case CHARGING: - break; - } - } - - /* - * Stop early if charging is disabled - */ - if (!config.enabledCharging()) { - evcs.setChargePowerLimit(0); - return; - } - - int nextChargePower = 0; - int nextMinPower = 0; - - /* - * Calculates the next charging power depending on the charge mode - */ - switch (config.chargeMode()) { - case EXCESS_POWER: - /* - * Get the next charge power depending on the priority. - */ - switch (config.priority()) { - case CAR: - nextChargePower = this.calculateChargePowerFromExcessPower(evcs); - break; - - case STORAGE: - int storageSoc = this.sum.getEssSoc().orElse(0); - if (storageSoc > 97) { - nextChargePower = this.calculateChargePowerFromExcessPower(evcs); - } else { - nextChargePower = this.calculateExcessPowerAfterEss(evcs, ess); - } - break; - } - - evcs._setMinimumPower(config.defaultChargeMinPower()); - nextMinPower = config.defaultChargeMinPower(); - break; - - case FORCE_CHARGE: - evcs._setMinimumPower(0); - nextChargePower = config.forceChargeMinPower() * evcs.getPhases().orElse(3); - break; - } - - if (nextChargePower < nextMinPower) { - nextChargePower = nextMinPower; - } - - /** - * Distribute next charge power on EVCS. - */ - if (isClustered) { - if (nextChargePower != 0) { - - int chargePower = evcs.getChargePower().orElse(0); - - // Check difference of the current charging and the previous charging target - if (this.chargingLowerThanTargetHandler.isLower(evcs)) { - if (chargePower <= evcs.getMinimumHardwarePower().orElse(1380)) { - nextChargePower = 0; - } else { - nextChargePower = (chargePower + CHARGE_POWER_BUFFER); - evcs._setMaximumPower(nextChargePower); - - this.logDebug(this.log, "Set a lower charging target of " + nextChargePower + " W"); - } - } else { - int currMax = evcs.getMaximumPower().orElse(0); - - if (chargePower > currMax * (1 + DEFAULT_UPPER_TARGET_DIFFERENCE_PERCENT)) { - evcs._setMaximumPower(evcs.getMaximumHardwarePower().getOrError()); - } - } - } - - evcs.setChargePowerRequest(nextChargePower); - } else { - evcs.setChargePowerLimit(nextChargePower); - } - this.logDebug(this.log, "Next charge power: " + nextChargePower + " W"); - } - - /** - * Calculates the next charging power, depending on the current PV production - * and house consumption. - * - * @param evcs Electric Vehicle Charging Station - * @return the available excess power for charging - * @throws OpenemsNamedException on error - */ - private int calculateChargePowerFromExcessPower(ManagedEvcs evcs) throws OpenemsNamedException { - int nextChargePower; - - int buyFromGrid = this.sum.getGridActivePower().orElse(0); - int essDischarge = this.sum.getEssActivePower().orElse(0); - int essActivePowerDC = this.sum.getProductionDcActualPower().orElse(0); - int evcsCharge = evcs.getChargePower().orElse(0); - - int excessPower = evcsCharge - buyFromGrid - (essDischarge - essActivePowerDC); - - nextChargePower = excessPower; - - Channel minimumHardwarePowerChannel = evcs.channel(Evcs.ChannelId.MINIMUM_HARDWARE_POWER); - if (nextChargePower < minimumHardwarePowerChannel.value().orElse(0)) { /* charging under 6A isn't possible */ - nextChargePower = 0; - } - return nextChargePower; - } - - /** - * Calculates the next charging power from excess power after Ess charging. - * - * @param evcs the ManagedEvcs - * @param ess the ManagedSymmetricEss - * @return the available excess power for charging - */ - private int calculateExcessPowerAfterEss(ManagedEvcs evcs, SymmetricEss ess) { - int maxEssCharge; - if (ess instanceof ManagedSymmetricEss) { - ManagedSymmetricEss e = (ManagedSymmetricEss) ess; - Power power = ((ManagedSymmetricEss) ess).getPower(); - maxEssCharge = power.getMinPower(e, Phase.ALL, Pwr.ACTIVE); - maxEssCharge = Math.abs(maxEssCharge); - } else { - maxEssCharge = ess.getMaxApparentPower().orElse(0); - } - int buyFromGrid = this.sum.getGridActivePower().orElse(0); - int essActivePower = this.sum.getEssActivePower().orElse(0); - int essActivePowerDC = this.sum.getProductionDcActualPower().orElse(0); - int evcsCharge = evcs.getChargePower().orElse(0); - int result = -buyFromGrid + evcsCharge - (maxEssCharge + (essActivePower - essActivePowerDC)); - result = result > 0 ? result : 0; - - return result; - } - - @Override - public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { - return new ModbusSlaveTable(// - OpenemsComponent.getModbusSlaveNatureTable(accessMode), // - Controller.getModbusSlaveNatureTable(accessMode)); - } - - public void configUpdate(String targetProperty, Object requiredValue) { - // final String targetProperty = property + ".target"; - Configuration c; - try { - String pid = this.servicePid(); - if (pid.isEmpty()) { - this.logInfo(log, "PID of " + this.id() + " is Empty"); - return; - } - c = cm.getConfiguration(pid, "?"); - Dictionary properties = c.getProperties(); - Object target = properties.get(targetProperty); - String existingTarget = target.toString(); - if (!existingTarget.isEmpty()) { - properties.put(targetProperty, requiredValue); - c.update(properties); - } - } catch (IOException | SecurityException e) { - this.logError(log, "ERROR: " + e.getMessage()); - } - } - - @Override - public void logInfo(Logger log, String message) { - super.logInfo(log, message); - } - - @Override - protected void logWarn(Logger log, String message) { - super.logWarn(log, message); - } - - @Override - protected void logDebug(Logger log, String message) { - if (this.config.debugMode()) { - this.logInfo(this.log, message); - } - } -} diff --git a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/EvcsControllerImplTest.java b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/EvcsControllerImplTest.java deleted file mode 100644 index b70742891ec..00000000000 --- a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/EvcsControllerImplTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package io.openems.edge.controller.evcs; - -import org.junit.Test; - -import io.openems.common.types.ChannelAddress; -import io.openems.edge.common.component.ComponentManager; -import io.openems.edge.common.sum.DummySum; -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.controller.test.ControllerTest; -import io.openems.edge.ess.test.DummyManagedSymmetricEss; -import io.openems.edge.evcs.api.Status; -import io.openems.edge.evcs.test.DummyManagedEvcs; - -public class EvcsControllerImplTest { - - private static final String SUM_ID = "_sum"; - private static final ChannelAddress SUM_GRID_ACTIVE_POWER = new ChannelAddress(SUM_ID, "GridActivePower"); - private static final ChannelAddress SUM_ESS_ACTIVE_POWER = new ChannelAddress(SUM_ID, "EssActivePower"); - - private static final String CTRL_ID = "ctrl0"; - - private static final String ESS_ID = "ess0"; - private static final ChannelAddress ESS_ALLOWED_CHARGE_POWER = new ChannelAddress(ESS_ID, "AllowedChargePower"); - - private static final String EVCS_ID = "evcs0"; - private static final ChannelAddress EVCS_CHARGE_POWER = new ChannelAddress(EVCS_ID, "ChargePower"); - private static final ChannelAddress EVCS_SET_CHARGE_POWER_LIMIT = new ChannelAddress(EVCS_ID, - "SetChargePowerLimit"); - private static final ChannelAddress EVCS_SET_CHARGE_POWER_REQUEST = new ChannelAddress(EVCS_ID, - "SetChargePowerRequest"); - private static final ChannelAddress EVCS_MAXIMUM_POWER = new ChannelAddress(EVCS_ID, "MaximumPower"); - private static final ChannelAddress EVCS_IS_CLUSTERED = new ChannelAddress(EVCS_ID, "IsClustered"); - private static final ChannelAddress EVCS_STATUS = new ChannelAddress(EVCS_ID, "Status"); - - @Test - public void excessChargeTest1() throws Exception { - new ControllerTest(new EvcsControllerImpl()) // - .addReference("componentManager", new DummyComponentManager()) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("sum", new DummySum()) // - .addReference("evcs", new DummyManagedEvcs(EVCS_ID)) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID) // - .withSoc(20) // - .withCapacity(9000)) // - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setEvcsId(EVCS_ID) // - .setDebugMode(false) // - .setEnabledCharging(true) // - .setChargeMode(ChargeMode.EXCESS_POWER) // - .setForceChargeMinPower(3333) // - .setEnergySessionLimit(0) // - .setPriority(Priority.CAR) // - .build()) - .next(new TestCase() // - .input(SUM_ESS_ACTIVE_POWER, -6000) // - .input(SUM_GRID_ACTIVE_POWER, 0) // - .input(EVCS_CHARGE_POWER, 0) // - .output(EVCS_SET_CHARGE_POWER_LIMIT, 6000)); // - } - - // TODO needs fix by Sebastian Asen - // @Test - protected void excessChargeTest2() throws Exception { - new ControllerTest(new EvcsControllerImpl()) // - .addReference("componentManager", new DummyComponentManager()) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("sum", new DummySum()) // - .addReference("evcs", new DummyManagedEvcs(EVCS_ID)) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID) // - .withSoc(20) // - .withCapacity(9000) // - .withMaxApparentPower(30_000)) // - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setEvcsId(EVCS_ID) // - .setDebugMode(false) // - .setEnabledCharging(true) // - .setChargeMode(ChargeMode.EXCESS_POWER) // - .setForceChargeMinPower(3333) // - .setEnergySessionLimit(0) // - .setPriority(Priority.STORAGE) // - .build()) - .next(new TestCase() // - .input(SUM_ESS_ACTIVE_POWER, -5000) // - .input(SUM_GRID_ACTIVE_POWER, -40000) // - .input(EVCS_CHARGE_POWER, 5000) // - .input(ESS_ALLOWED_CHARGE_POWER, 30000) // - .output(EVCS_SET_CHARGE_POWER_LIMIT, 20000)); - } - - @Test - public void clusterTest() throws Exception { - final ComponentManager componentManager = new DummyComponentManager(); - new ControllerTest(new EvcsControllerImpl()) // - .addReference("componentManager", componentManager) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("sum", new DummySum()) // - .addReference("evcs", new DummyManagedEvcs(EVCS_ID)) // - .addComponent(new DummyManagedSymmetricEss(ESS_ID) // - .withSoc(20) // - .withCapacity(9000) // - .withMaxApparentPower(30_000)) // - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setEvcsId(EVCS_ID) // - .setDebugMode(false) // - .setEnabledCharging(true) // - .setChargeMode(ChargeMode.EXCESS_POWER) // - .setForceChargeMinPower(3333) // - .setEnergySessionLimit(0) // - .setPriority(Priority.CAR) // - .build()) - .next(new TestCase() // - .input(SUM_ESS_ACTIVE_POWER, -10000) // - .input(SUM_GRID_ACTIVE_POWER, 0) // - .input(EVCS_CHARGE_POWER, 0) // - .input(EVCS_MAXIMUM_POWER, 6000) // - .input(EVCS_IS_CLUSTERED, true) // - .input(EVCS_STATUS, Status.READY_FOR_CHARGING) // - .output(EVCS_SET_CHARGE_POWER_REQUEST, 10000)); - } -} diff --git a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/EvcsControllerTest.java b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/EvcsControllerTest.java new file mode 100644 index 00000000000..cc261bb9bee --- /dev/null +++ b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/EvcsControllerTest.java @@ -0,0 +1,313 @@ +package io.openems.edge.controller.evcs; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.filter.DisabledRampFilter; +import io.openems.edge.common.sum.DummySum; +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.controller.test.ControllerTest; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.ess.test.DummyPower; +import io.openems.edge.evcs.api.Status; +import io.openems.edge.evcs.test.DummyEvcsPower; +import io.openems.edge.evcs.test.DummyManagedEvcs; + +public class EvcsControllerTest { + + private static final DummyPower POWER = new DummyPower(30000); + private static final DummyManagedSymmetricEss ESS = new DummyManagedSymmetricEss("ess0", POWER); + private static final DummyEvcsPower EVCS_POWER = new DummyEvcsPower(new DisabledRampFilter()); + private static final DummyManagedEvcs EVCS = new DummyManagedEvcs("evcs0", EVCS_POWER); + + private static String EVCS_ID = EVCS.id(); + private static boolean ENABLE_CHARGING; + private static ChargeMode CHARGE_MODE; + private static int FORCE_CHARGE_MIN_POWER = 7360; + private static int DEFAULT_CHARGE_MIN_POWER = 0; + private static Priority PRIORITY = Priority.CAR; + private static String ESS_ID = "ess0"; + private static int ENERGY_SESSION_LIMIT = 0; + + private static ChannelAddress sumGridActivePower = new ChannelAddress("_sum", "GridActivePower"); + private static ChannelAddress sumEssActivePower = new ChannelAddress("_sum", "EssActivePower"); + private static ChannelAddress evcs0ChargePower = new ChannelAddress("evcs0", "ChargePower"); + private static ChannelAddress evcs0SetChargePowerLimit = new ChannelAddress("evcs0", "SetChargePowerLimit"); + private static ChannelAddress essAllowedChargePower = new ChannelAddress("ess0", "AllowedChargePower"); + private static ChannelAddress evcs0MaximumPower = new ChannelAddress("evcs0", "MaximumPower"); + private static ChannelAddress evcs0IsClustered = new ChannelAddress("evcs0", "IsClustered"); + private static ChannelAddress evcs0SetPowerRequest = new ChannelAddress("evcs0", "SetChargePowerRequest"); + private static ChannelAddress evcs0Status = new ChannelAddress("evcs0", "Status"); + private static ChannelAddress evcs0MaximumHardwarePower = new ChannelAddress("evcs0", "MaximumHardwarePower"); + + @Test + public void excessChargeTest1() throws Exception { + + ENABLE_CHARGING = true; + CHARGE_MODE = ChargeMode.EXCESS_POWER; + PRIORITY = Priority.CAR; + + new ControllerTest(new EvcsController()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("evcs", EVCS) // + .addReference("ess", ESS) // + .activate(MyConfig.create() // + .setId("ctrlEvcs0") // + .setEvcsId(EVCS_ID) // + .setEnableCharging(ENABLE_CHARGING) // + .setChargeMode(CHARGE_MODE) // + .setForceChargeMinPower(FORCE_CHARGE_MIN_POWER) // + .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // + .setPriority(PRIORITY) // + .setEssId(ESS_ID) // + .setEnergySessionLimit(ENERGY_SESSION_LIMIT) // + .build()) // + .next(new TestCase().input(sumEssActivePower, -6000) // + .input(evcs0IsClustered, false) // + .input(sumGridActivePower, 0) // + .input(evcs0ChargePower, 0) // + .output(evcs0SetChargePowerLimit, 6000)) // + ; + } + + @Test + public void excessChargeTest2() throws Exception { + + ENABLE_CHARGING = true; + CHARGE_MODE = ChargeMode.EXCESS_POWER; + PRIORITY = Priority.STORAGE; + + new ControllerTest(new EvcsController()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("evcs", EVCS) // + .addReference("ess", ESS) // + .activate(MyConfig.create() // + .setId("ctrlEvcs0") // + .setEvcsId(EVCS_ID) // + .setEnableCharging(ENABLE_CHARGING) // + .setChargeMode(CHARGE_MODE) // + .setForceChargeMinPower(FORCE_CHARGE_MIN_POWER) // + .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // + .setPriority(PRIORITY) // + .setEssId(ESS_ID) // + .setEnergySessionLimit(ENERGY_SESSION_LIMIT) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, -5000) // + .input(evcs0IsClustered, false) // + .input(sumGridActivePower, -40000) // + .input(evcs0ChargePower, 5000) // + .input(essAllowedChargePower, 30000) // + .input(evcs0MaximumHardwarePower, 22080) // + .output(evcs0SetChargePowerLimit, 20000)) + + ; + } + + @Test + public void forceChargeTest() throws Exception { + + ENABLE_CHARGING = true; + FORCE_CHARGE_MIN_POWER = 7360; + CHARGE_MODE = ChargeMode.FORCE_CHARGE; + PRIORITY = Priority.CAR; + + new ControllerTest(new EvcsController()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("evcs", EVCS) // + .addReference("ess", ESS) // + .activate(MyConfig.create() // + .setId("ctrlEvcs0") // + .setEvcsId(EVCS_ID) // + .setEnableCharging(ENABLE_CHARGING) // + .setChargeMode(CHARGE_MODE) // + .setForceChargeMinPower(FORCE_CHARGE_MIN_POWER) // + .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // + .setPriority(PRIORITY) // + .setEssId(ESS_ID) // + .setEnergySessionLimit(ENERGY_SESSION_LIMIT) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, -5000) // + .input(evcs0IsClustered, false) // + .input(sumGridActivePower, -40000) // + .input(evcs0ChargePower, 5000) // + .input(essAllowedChargePower, 30000) // + .output(evcs0SetChargePowerLimit, 22080)); + } + + @Test + public void chargingDisabledTest() throws Exception { + + ENABLE_CHARGING = false; + + new ControllerTest(new EvcsController()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("evcs", EVCS) // + .addReference("ess", ESS) // + .activate(MyConfig.create() // + .setId("ctrlEvcs0") // + .setEvcsId(EVCS_ID) // + .setEnableCharging(ENABLE_CHARGING) // + .setChargeMode(CHARGE_MODE) // + .setForceChargeMinPower(FORCE_CHARGE_MIN_POWER) // + .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // + .setPriority(PRIORITY) // + .setEssId(ESS_ID) // + .setEnergySessionLimit(ENERGY_SESSION_LIMIT) // + .build()) // + .next(new TestCase() // + .output(evcs0SetChargePowerLimit, 0)); + } + + @Test + public void wrongConfigParametersTest() throws Exception { + + DEFAULT_CHARGE_MIN_POWER = 30000; + FORCE_CHARGE_MIN_POWER = 30000; + + DummyConfigurationAdmin cm = new DummyConfigurationAdmin(); + new ControllerTest(new EvcsController()) // + .addReference("cm", cm) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("evcs", EVCS) // + .addReference("ess", ESS) // + .activate(MyConfig.create() // + .setId("ctrlEvcs0") // + .setEvcsId(EVCS_ID) // + .setEnableCharging(ENABLE_CHARGING) // + .setChargeMode(CHARGE_MODE) // + .setForceChargeMinPower(FORCE_CHARGE_MIN_POWER) // + .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // + .setPriority(PRIORITY) // + .setEssId(ESS_ID) // + .setEnergySessionLimit(ENERGY_SESSION_LIMIT) // + .build()) // + .next(new TestCase() // + .input(evcs0MaximumHardwarePower, 12000)); + + assertEquals(12000, + (int) (Integer) cm.getConfiguration("ctrlEvcs0").getProperties().get("defaultChargeMinPower")); + } + + @Test + public void clusterTest() throws Exception { + + ENABLE_CHARGING = true; + FORCE_CHARGE_MIN_POWER = 3333; + CHARGE_MODE = ChargeMode.EXCESS_POWER; + PRIORITY = Priority.CAR; + + new ControllerTest(new EvcsController()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("evcs", EVCS) // + .addReference("ess", ESS) // + .activate(MyConfig.create() // + .setId("ctrlEvcs0") // + .setEvcsId(EVCS_ID) // + .setEnableCharging(ENABLE_CHARGING) // + .setChargeMode(CHARGE_MODE) // + .setForceChargeMinPower(FORCE_CHARGE_MIN_POWER) // + .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // + .setPriority(PRIORITY) // + .setEssId(ESS_ID) // + .setEnergySessionLimit(ENERGY_SESSION_LIMIT) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, -10000) // + .input(evcs0IsClustered, true) // + .input(sumGridActivePower, 0) // + .input(evcs0ChargePower, 0) // + .input(evcs0Status, Status.CHARGING) // + .output(evcs0SetPowerRequest, 10000)) + .next(new TestCase().input(sumEssActivePower, -6000) // + .input(evcs0IsClustered, true) // + .input(sumGridActivePower, 0) // + .input(evcs0ChargePower, 0) // + .input(evcs0Status, Status.NOT_READY_FOR_CHARGING) // + .output(evcs0SetPowerRequest, 0)) // + .next(new TestCase().input(sumEssActivePower, -6000) // + .input(evcs0IsClustered, true) // + .input(sumGridActivePower, 0) // + .input(evcs0ChargePower, 0) // + .input(evcs0Status, null) // + .output(evcs0SetPowerRequest, 0)) // + .next(new TestCase().input(sumEssActivePower, -6000) // + .input(evcs0IsClustered, true) // + .input(sumGridActivePower, 0) // + .input(evcs0ChargePower, 0) // + .input(evcs0Status, Status.CHARGING_REJECTED) // + .output(evcs0SetPowerRequest, 6000) // + .output(evcs0MaximumPower, null)) // + ; + } + + @Test + public void clusterTestDisabledCharging() throws Exception { + + ENABLE_CHARGING = false; + FORCE_CHARGE_MIN_POWER = 3333; + CHARGE_MODE = ChargeMode.EXCESS_POWER; + PRIORITY = Priority.CAR; + + new ControllerTest(new EvcsController()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("evcs", EVCS) // + .addReference("ess", ESS) // + .activate(MyConfig.create() // + .setId("ctrlEvcs0") // + .setEvcsId(EVCS_ID) // + .setEnableCharging(ENABLE_CHARGING) // + .setChargeMode(CHARGE_MODE) // + .setForceChargeMinPower(FORCE_CHARGE_MIN_POWER) // + .setDefaultChargeMinPower(DEFAULT_CHARGE_MIN_POWER) // + .setPriority(PRIORITY) // + .setEssId(ESS_ID) // + .setEnergySessionLimit(ENERGY_SESSION_LIMIT) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, -10000) // + .input(evcs0IsClustered, true) // + .input(sumGridActivePower, 0) // + .input(evcs0ChargePower, 0) // + .input(evcs0Status, Status.CHARGING) // + .output(evcs0SetChargePowerLimit, 0)) + .next(new TestCase().input(sumEssActivePower, -6000) // + .input(evcs0IsClustered, true) // + .input(sumGridActivePower, 0) // + .input(evcs0ChargePower, 0) // + .input(evcs0Status, Status.NOT_READY_FOR_CHARGING) // + .output(evcs0SetChargePowerLimit, 0)) // + .next(new TestCase().input(sumEssActivePower, -6000) // + .input(evcs0IsClustered, true) // + .input(sumGridActivePower, 0) // + .input(evcs0ChargePower, 0) // + .input(evcs0Status, null) // + .output(evcs0SetChargePowerLimit, 0)) // + .next(new TestCase().input(sumEssActivePower, -6000) // + .input(evcs0IsClustered, true) // + .input(sumGridActivePower, 0) // + .input(evcs0ChargePower, 0) // + .input(evcs0Status, Status.CHARGING_REJECTED) // + .output(evcs0SetChargePowerLimit, 0) // + .output(evcs0MaximumPower, null)) // + ; + } +} diff --git a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/MyConfig.java b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/MyConfig.java index 4b4fad8995b..6e924681469 100644 --- a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/MyConfig.java +++ b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/MyConfig.java @@ -1,22 +1,24 @@ package io.openems.edge.controller.evcs; -import io.openems.common.utils.ConfigUtils; +import io.openems.edge.common.startstop.StartStopConfig; import io.openems.edge.common.test.AbstractComponentConfig; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { protected static class Builder { - private String id = null; - private String essId = null; - public boolean debugMode; - public String evcsId; - public boolean enabledCharging; - public ChargeMode chargeMode; - public int forceChargeMinPower; - public int defaultChargeMinPower; - public Priority priority; - public int energySessionLimit; + private String id; + private String alias = "Controller Evcs"; + private boolean enabled = true; + private boolean debugMode = false; + private String evcsId = "evcs0"; + private boolean enabledCharging = true; + private ChargeMode chargeMode = ChargeMode.FORCE_CHARGE; + private int forceChargeMinPower = 7560; + private int defaultChargeMinPower = 0; + private Priority priority = Priority.CAR; + private String essId = "ess0"; + private int energySessionLimit = 0; private Builder() { } @@ -26,23 +28,28 @@ public Builder setId(String id) { return this; } - public Builder setEssId(String essId) { - this.essId = essId; + public Builder setAlias(String alias) { + this.alias = alias; return this; } + public Builder setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + public Builder setDebugMode(boolean debugMode) { this.debugMode = debugMode; return this; } - + public Builder setEvcsId(String evcsId) { this.evcsId = evcsId; return this; } - public Builder setEnabledCharging(boolean enabledCharging) { - this.enabledCharging = enabledCharging; + public Builder setEnableCharging(boolean enableCharging) { + this.enabledCharging = enableCharging; return this; } @@ -66,21 +73,20 @@ public Builder setPriority(Priority priority) { return this; } + public Builder setEssId(String essId) { + this.essId = essId; + return this; + } public Builder setEnergySessionLimit(int energySessionLimit) { this.energySessionLimit = energySessionLimit; return this; } - + public MyConfig build() { return new MyConfig(this); } } - /** - * Create a Config builder. - * - * @return a {@link Builder} - */ public static Builder create() { return new Builder(); } @@ -92,14 +98,10 @@ private MyConfig(Builder builder) { this.builder = builder; } - @Override - public String ess_id() { - return this.builder.essId; - } @Override - public boolean debugMode() { - return this.builder.debugMode; + public String id() { + return this.builder.id; } @Override @@ -132,9 +134,23 @@ public Priority priority() { return this.builder.priority; } + @Override + public String ess_id() { + return this.builder.essId; + } + @Override public int energySessionLimit() { return this.builder.energySessionLimit; } -} \ No newline at end of file + @Override + public boolean debugMode() { + return this.builder.debugMode; + } + + @Override + public String evcs_target() { + return "(&(enabled=true)(!(service.pid=ctrlEvcs0))(|(id=" + this.evcs_id() + ")))"; + } +} diff --git a/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/PowerComponentImpl.java b/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/PowerComponentImpl.java index d84529ea4f5..1dfc80865d6 100644 --- a/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/PowerComponentImpl.java +++ b/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/PowerComponentImpl.java @@ -25,6 +25,7 @@ import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; +import io.openems.edge.common.filter.DisabledPidFilter; import io.openems.edge.common.filter.PidFilter; import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.ess.core.power.data.ConstraintUtil; diff --git a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/Evcs.java b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/Evcs.java index 08300d5007c..bb13f768467 100644 --- a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/Evcs.java +++ b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/Evcs.java @@ -7,6 +7,7 @@ import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.channel.IntegerReadChannel; +import io.openems.edge.common.channel.LongReadChannel; import io.openems.edge.common.channel.StateChannel; import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; @@ -151,6 +152,19 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .unit(Unit.WATT_HOURS) // .accessMode(AccessMode.READ_ONLY)), + /** + * Active Consumption Energy. + * + *
    + *
  • Interface: Evcs + *
  • Type: Integer + *
  • Unit: Wh + *
+ */ + ACTIVE_CONSUMPTION_ENERGY(Doc.of(OpenemsType.LONG) // + .unit(Unit.WATT_HOURS) // + .accessMode(AccessMode.READ_ONLY)), + /** * Failed state channel for a failed communication to the EVCS. * @@ -498,6 +512,45 @@ public default void _setEnergySession(Integer value) { public default void _setEnergySession(int value) { this.getEnergySessionChannel().setNextValue(value); } + + /** + * Gets the Channel for {@link ChannelId#ACTIVE_CONSUMPTION_ENERGY}. + * + * @return the Channel + */ + public default LongReadChannel getActiveConsumptionEnergyChannel() { + return this.channel(ChannelId.ACTIVE_CONSUMPTION_ENERGY); + } + + /** + * Gets the Active Consumption Energy in [Wh]. This relates to negative + * ACTIVE_POWER. See {@link ChannelId#ACTIVE_CONSUMPTION_ENERGY}. + * + * @return the Channel {@link Value} + */ + public default Value getActiveConsumptionEnergy() { + return this.getActiveConsumptionEnergyChannel().value(); + } + + /** + * Internal method to set the 'nextValue' on + * {@link ChannelId#ACTIVE_CONSUMPTION_ENERGY} Channel. + * + * @param value the next value + */ + public default void _setActiveConsumptionEnergy(Long value) { + this.getActiveConsumptionEnergyChannel().setNextValue(value); + } + + /** + * Internal method to set the 'nextValue' on + * {@link ChannelId#ACTIVE_CONSUMPTION_ENERGY} Channel. + * + * @param value the next value + */ + public default void _setActiveConsumptionEnergy(long value) { + this.getActiveConsumptionEnergyChannel().setNextValue(value); + } /** * Gets the Channel for {@link ChannelId#CHARGINGSTATION_COMMUNICATION_FAILED}. diff --git a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/EvcsPower.java b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/EvcsPower.java new file mode 100644 index 00000000000..a60c79bf859 --- /dev/null +++ b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/EvcsPower.java @@ -0,0 +1,13 @@ +package io.openems.edge.evcs.api; + +import io.openems.edge.common.filter.RampFilter; + +public interface EvcsPower { + + /** + * Gets the RampFilter instance with the configured variables. + * + * @return an instance of {@link RampFilter} + */ + public RampFilter getRampFilter(); +} diff --git a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/ManagedEvcs.java b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/ManagedEvcs.java index 036b17bb320..d781ad5eb35 100644 --- a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/ManagedEvcs.java +++ b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/ManagedEvcs.java @@ -8,15 +8,19 @@ import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.BooleanReadChannel; import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.IntegerDoc; import io.openems.edge.common.channel.IntegerWriteChannel; import io.openems.edge.common.channel.StringWriteChannel; import io.openems.edge.common.channel.value.Value; +import io.openems.edge.common.filter.RampFilter; import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; import io.openems.edge.common.modbusslave.ModbusType; @ProviderType public interface ManagedEvcs extends Evcs { + public EvcsPower getEvcsPower(); + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { /** @@ -33,7 +37,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { * * *

- * Function + * Function: *

    *
  • Write Value should be sent to the EVCS and cleared afterwards *
  • Read value should contain the currently valid loading target that was @@ -51,6 +55,37 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .unit(Unit.WATT) // .accessMode(AccessMode.READ_WRITE)), + /** + * Applies the PID filter and then sets a fixed Active Power. + * + *
      + *
    • Interface: ManagedEvcs + *
    • Writable + *
    • Type: Integer + *
    • Unit: W + *
    + */ + SET_CHARGE_POWER_LIMIT_WITH_PID(new IntegerDoc() // + .unit(Unit.WATT) // + .accessMode(AccessMode.READ_WRITE).onInit(channel -> { + ((IntegerWriteChannel) channel).onSetNextWrite(value -> { + + if (value != null) { + ManagedEvcs evcs = (ManagedEvcs) channel.getComponent(); + + RampFilter rampFilter = evcs.getEvcsPower().getRampFilter(); + + rampFilter.setLimits(evcs.getMinimumHardwarePower().orElse(0), + evcs.getMaximumHardwarePower().orElse(value)); + + int currentPower = evcs.getChargePower().orElse(0); + int pidOutput = rampFilter.applyRampFilter(currentPower, value); + + evcs.setChargePowerLimit(pidOutput); + } + }); + })), // + /** * Is true if the EVCS is in a EVCS-Cluster. * @@ -170,6 +205,56 @@ public default void setChargePowerLimit(Integer value) throws OpenemsNamedExcept this.getSetChargePowerLimitChannel().setNextWriteValue(value); } + /** + * Gets the Channel for {@link ChannelId#SET_CHARGE_POWER_LIMIT_WITH_PID}. + * + * @return the Channel + */ + public default IntegerWriteChannel getSetChargePowerLimitWithPidChannel() { + return this.channel(ChannelId.SET_CHARGE_POWER_LIMIT_WITH_PID); + } + + /** + * Gets the set charge power limit of the EVCS in [W] with applied PID filter. + * See {@link ChannelId#SET_CHARGE_POWER_LIMIT_WITH_PID}. + * + * @return the Channel {@link Value} + */ + public default Value getSetChargePowerLimitWithPid() { + return this.getSetChargePowerLimitWithPidChannel().value(); + } + + /** + * Internal method to set the 'nextValue' on + * {@link ChannelId#SET_CHARGE_POWER_LIMIT_WITH_PID} Channel. + * + * @param value the next value + */ + public default void _setSetChargePowerLimitWithPid(Integer value) { + this.getSetChargePowerLimitWithPidChannel().setNextValue(value); + } + + /** + * Internal method to set the 'nextValue' on + * {@link ChannelId#SET_CHARGE_POWER_LIMIT_WITH_PID} Channel. + * + * @param value the next value + */ + public default void _setSetChargePowerLimitWithPid(int value) { + this.getSetChargePowerLimitWithPidChannel().setNextValue(value); + } + + /** + * Sets the charge power limit of the EVCS in [W] with applied PID filter. See + * {@link ChannelId#SET_CHARGE_POWER_LIMIT_WITH_PID}. + * + * @param value the next write value + * @throws OpenemsNamedException on error + */ + public default void setChargePowerLimitWithPid(Integer value) throws OpenemsNamedException { + this.getSetChargePowerLimitWithPidChannel().setNextWriteValue(value); + } + /** * Gets the Channel for {@link ChannelId#IS_CLUSTERED}. * diff --git a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/MeasuringEvcs.java b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/MeasuringEvcs.java index 4748aa9870d..21886aceda6 100644 --- a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/MeasuringEvcs.java +++ b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/api/MeasuringEvcs.java @@ -221,9 +221,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { *
  • Unit: Hz *
*/ - FREQUENCY(Doc.of(OpenemsType.STRING).unit(Unit.HERTZ).accessMode(AccessMode.READ_ONLY) - .text("Frequency")), - + FREQUENCY(Doc.of(OpenemsType.STRING).unit(Unit.HERTZ).accessMode(AccessMode.READ_ONLY).text("Frequency")), + /** * Active power to grid (export) * @@ -329,7 +328,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { * */ VOLTAGE(Doc.of(OpenemsType.STRING).accessMode(AccessMode.READ_ONLY).text("Voltage")), - + /** * Temperature. * diff --git a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyEvcsPower.java b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyEvcsPower.java new file mode 100644 index 00000000000..a42e713d696 --- /dev/null +++ b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyEvcsPower.java @@ -0,0 +1,23 @@ +package io.openems.edge.evcs.test; + +import io.openems.edge.common.filter.RampFilter; +import io.openems.edge.evcs.api.EvcsPower; + +public class DummyEvcsPower implements EvcsPower { + + private final RampFilter rampFilter; + + public DummyEvcsPower(RampFilter rampFilter) { + this.rampFilter = rampFilter; + } + + public DummyEvcsPower() { + this(new RampFilter()); + } + + @Override + public RampFilter getRampFilter() { + return this.rampFilter; + } + +} diff --git a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyManagedEvcs.java b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyManagedEvcs.java index 96e6224963b..1276a132ae4 100644 --- a/io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyManagedEvcs.java +++ b/io.openems.edge.evcs.api/src/io/openems/edge/evcs/test/DummyManagedEvcs.java @@ -4,24 +4,33 @@ import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.evcs.api.Evcs; +import io.openems.edge.evcs.api.EvcsPower; import io.openems.edge.evcs.api.ManagedEvcs; public class DummyManagedEvcs extends AbstractOpenemsComponent implements Evcs, ManagedEvcs, OpenemsComponent { + private final EvcsPower evcsPower; + /** * Constructor. * * @param id id */ - public DummyManagedEvcs(String id) { + public DummyManagedEvcs(String id, EvcsPower evcsPower) { super(// OpenemsComponent.ChannelId.values(), // ManagedEvcs.ChannelId.values(), // Evcs.ChannelId.values() // ); + this.evcsPower = evcsPower; for (Channel channel : this.channels()) { channel.nextProcessImage(); } super.activate(null, id, "", true); } + + @Override + public EvcsPower getEvcsPower() { + return this.evcsPower; + } } diff --git a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/AbstractEvcsCluster.java b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/AbstractEvcsCluster.java index 780a815abd2..30e73638584 100644 --- a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/AbstractEvcsCluster.java +++ b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/AbstractEvcsCluster.java @@ -10,7 +10,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.openems.common.channel.Unit; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.Doc; import io.openems.edge.common.channel.calculate.CalculateIntegerSum; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; @@ -32,6 +35,28 @@ public AbstractEvcsCluster(io.openems.edge.common.channel.ChannelId[] firstIniti super(firstInitialChannelIds, furtherInitialChannelIds); } + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + MAXIMUM_POWER_TO_DISTRIBUTE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT).text("Maximum power to distribute, for all given Evcss.")), + MAXIMUM_AVAILABLE_ESS_POWER(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT).text("Maximum available ess power.")), + MAXIMUM_AVAILABLE_GRID_POWER(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT).text("Maximum available grid power.")), + USED_ESS_MAXIMUM_DISCHARGE_POWER(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT).text("Dynamic maximum discharge power, that could be limited by us to ensure the possibillity to discharge the battery.")); + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + @Override protected void activate(ComponentContext context, String id, String alias, boolean enabled) { super.activate(context, id, alias, enabled); @@ -93,6 +118,7 @@ private void calculateChannelValues() { protected void limitEvcss() { try { int totalPowerLimit = this.getMaximumPowerToDistribute(); + this.channel(ChannelId.MAXIMUM_POWER_TO_DISTRIBUTE).setNextValue(totalPowerLimit); /* * If a maximum power is present, e.g. from another cluster, then the limit will @@ -117,39 +143,49 @@ protected void limitEvcss() { if (evcs instanceof ManagedEvcs) { ManagedEvcs managedEvcs = (ManagedEvcs) evcs; int requestedPower = managedEvcs.getSetChargePowerRequestChannel().getNextWriteValue().orElse(0); - Status status = evcs.getStatus(); + + if (requestedPower <= 0) { + managedEvcs.setChargePowerLimit(0); + continue; + } + + int guaranteedPower = getGuaranteedPower(managedEvcs); + Status status = managedEvcs.getStatus(); switch (status) { case CHARGING_FINISHED: - if (requestedPower > 0) { - managedEvcs.setChargePowerLimit(requestedPower); - } + managedEvcs.setChargePowerLimit(requestedPower); break; case ERROR: case STARTING: case UNDEFINED: case NOT_READY_FOR_CHARGING: case ENERGY_LIMIT_REACHED: + managedEvcs.setChargePowerLimit(0); + break; + case READY_FOR_CHARGING: + + // Check if there is enough power for an initial charge + if (totalPowerLimit - this.getChargePower().orElse(0) >= guaranteedPower) { + managedEvcs.setChargePowerLimit(guaranteedPower); + // TODO: managedEvcs._setStatus(Status.UNCONFIRMED_CHARGING); or put this in the + // setChargePowerLimit + totalPowerLeftMinusGuarantee -= guaranteedPower; + } break; // EVCS is active. case CHARGING_REJECTED: - case READY_FOR_CHARGING: case CHARGING: - - activeEvcss.add(managedEvcs); - /* * Reduces the available power by the guaranteed power of each charging station. * Sets the minimum power depending on the guaranteed and the maximum Power. */ - if (requestedPower > 0) { - int evcsMaxPower = evcs.getMaximumPower() - .orElse(evcs.getMaximumHardwarePower().orElse(DEFAULT_HARDWARE_LIMIT)); - int minGurarantee = this.getMinimumChargePowerGuarantee(); - int guarantee = evcsMaxPower > minGurarantee ? minGurarantee : evcsMaxPower; - - totalPowerLeftMinusGuarantee -= guarantee; - evcs._setMinimumPower(guarantee); + if (totalPowerLeftMinusGuarantee - guaranteedPower >= 0) { + totalPowerLeftMinusGuarantee -= guaranteedPower; + managedEvcs._setMinimumPower(guaranteedPower); + activeEvcss.add(managedEvcs); + } else { + managedEvcs.setChargePowerLimit(0); } } } @@ -160,11 +196,14 @@ protected void limitEvcss() { */ for (ManagedEvcs evcs : activeEvcss) { - int guaranteedPower = evcs.getMinimumPower().orElse(0); + // int guaranteedPower = evcs.getMinimumPowerChannel().getNextValue().orElse(0); + int guaranteedPower = evcs.getMinimumPowerChannel().getNextValue().orElse(0); // Power left for the this EVCS including its guaranteed power final int powerLeft = totalPowerLeftMinusGuarantee + guaranteedPower; + int maximumHardwareLimit = evcs.getMaximumHardwarePower().orElse(DEFAULT_HARDWARE_LIMIT); + int nextChargePower; Optional requestedPower = evcs.getSetChargePowerRequestChannel().getNextWriteValue(); @@ -174,32 +213,53 @@ protected void limitEvcss() { "Requested power ( for " + evcs.alias() + "): " + requestedPower.get()); nextChargePower = requestedPower.get(); } else { - nextChargePower = evcs.getMaximumHardwarePower().orElse(DEFAULT_HARDWARE_LIMIT); + nextChargePower = maximumHardwareLimit; } - // It should not be charged more than possible for the current EV - int maxPower = evcs.getMaximumPower() - .orElse(evcs.getMaximumHardwarePower().orElse(DEFAULT_HARDWARE_LIMIT)); - nextChargePower = nextChargePower > maxPower ? maxPower : nextChargePower; + // Total power should be only reduced by the maximum power, that EV is charging. + int maximumChargePower = evcs.getMaximumPower().orElse(nextChargePower); + + nextChargePower = nextChargePower > maximumHardwareLimit ? maximumHardwareLimit : nextChargePower; // Checks if there is enough power left and sets the charge power - if (nextChargePower < powerLeft) { - evcs.setChargePowerLimit(nextChargePower); - totalPowerLeftMinusGuarantee = totalPowerLeftMinusGuarantee - (nextChargePower - guaranteedPower); - this.logInfoInDebugmode(this.log, - "Charge power: " + nextChargePower + "; Power left: " + totalPowerLeftMinusGuarantee); + if (maximumChargePower < powerLeft) { + totalPowerLeftMinusGuarantee = totalPowerLeftMinusGuarantee + - (maximumChargePower - guaranteedPower); } else { - evcs.setChargePowerLimit(powerLeft); + nextChargePower = powerLeft; totalPowerLeftMinusGuarantee = 0; - this.logInfoInDebugmode(this.log, - "Power Left: " + totalPowerLeftMinusGuarantee + " ; Charge power: " + powerLeft); } + + /** + * Set the next charge power of the EVCS + */ + if (nextChargePower > evcs.getChargePower().orElse(0)) { + evcs.setChargePowerLimitWithPid(nextChargePower); + } else { + evcs.setChargePowerLimit(nextChargePower); + } + this.logInfoInDebugmode(this.log, "Next charge power: " + nextChargePower + "; Max charge power: " + + maximumChargePower + "; Power left: " + totalPowerLeftMinusGuarantee); } } catch (OpenemsNamedException e) { e.printStackTrace(); } } + /** + * Results the power that should be guaranteed for one EVCS. + * + * @param evcs EVCS whose limits should be used. + * @return Guaranteed power that should/can be used. + */ + private int getGuaranteedPower(Evcs evcs) { + int minGuarantee = this.getMinimumChargePowerGuarantee(); + int minHW = evcs.getMinimumHardwarePower().orElse(minGuarantee); + int evcsMaxPower = evcs.getMaximumPower().orElse(evcs.getMaximumHardwarePower().orElse(DEFAULT_HARDWARE_LIMIT)); + minGuarantee = evcsMaxPower > minGuarantee ? minGuarantee : evcsMaxPower; + return minHW > minGuarantee ? minHW : minGuarantee; + } + /** * Sorted list of the EVCSs in the cluster. * diff --git a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigPeakShaving.java b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigPeakShaving.java index b6ba770513a..5a8411b4be9 100644 --- a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigPeakShaving.java +++ b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigPeakShaving.java @@ -33,7 +33,16 @@ @AttributeDefinition(name = "Ess-ID", description = "ID of Ess device.") String ess_id() default "ess0"; - + + @AttributeDefinition(name = "Secure ess discharge enabled?", description = "Is secure ess discharge enabled?") + boolean enable_secure_ess_discharge() default true; + + @AttributeDefinition(name = "Start secure discharge SoC", description = "The allowed discharge power will be reduced softly under this SoC") + int ess_secure_discharge_soc() default 25; + + @AttributeDefinition(name = "Secure discharge minimum SoC", description = "The allowed discharge power will be reduced till this SoC") + int ess_secure_discharge_min_soc() default 15; + @AttributeDefinition(name = "Grid-Meter-ID", description = "ID of the Grid-Meter.") String meter_id() default "meter0"; diff --git a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java index 7d51fc52b83..13aff2efcce 100644 --- a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java +++ b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java @@ -68,10 +68,14 @@ public class EvcsClusterPeakShaving extends AbstractEvcsCluster implements Opene @Reference private ManagedSymmetricEss ess; + @Reference + private SymmetricMeter meter; + public EvcsClusterPeakShaving() { super(// OpenemsComponent.ChannelId.values(), // - Evcs.ChannelId.values()); + Evcs.ChannelId.values(), // + AbstractEvcsCluster.ChannelId.values()); } @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MULTIPLE) @@ -108,6 +112,9 @@ void activate(ComponentContext context, ConfigPeakShaving config) throws Openems if (OpenemsComponent.updateReferenceFilter(cm, this.servicePid(), "ess", config.ess_id())) { return; } + if (OpenemsComponent.updateReferenceFilter(cm, this.servicePid(), "meter", config.meter_id())) { + return; + } } @Deactivate @@ -176,20 +183,29 @@ public int getMaximumPowerToDistribute() { long maxAvailableStoragePower = 0; maxEssDischarge = this.ess.getAllowedDischargePower().orElse(0); + if (this.config.enable_secure_ess_discharge()) { + maxEssDischarge = this.getSecureEssDischargePower(maxEssDischarge); + this.channel(AbstractEvcsCluster.ChannelId.USED_ESS_MAXIMUM_DISCHARGE_POWER).setNextValue(maxEssDischarge); + } // TODO: Should I use power component here - // TODO: Calculate the available ESS charge power, depending on a specific ESS // component (e.g. If there is a ESS cluster) + + // Calculate maximum ess power long essDischargePower = this.sum.getEssActivePower().orElse(0); int essActivePowerDC = this.sum.getProductionDcActualPower().orElse(0); - maxAvailableStoragePower = maxEssDischarge - (essDischargePower - essActivePowerDC); + this.channel(AbstractEvcsCluster.ChannelId.MAXIMUM_AVAILABLE_ESS_POWER).setNextValue(maxAvailableStoragePower); + // Calculate maximum grid power int gridPower = getGridPower(); + int maxAvailableGridPower = (this.config.hardwarePowerLimitPerPhase() * DEFAULT_PHASES) - gridPower; + this.channel(AbstractEvcsCluster.ChannelId.MAXIMUM_AVAILABLE_GRID_POWER).setNextValue(maxAvailableGridPower); + + // Current evcs charge power int evcsCharge = this.getChargePower().orElse(0); - allowedChargePower = (int) (evcsCharge + maxAvailableStoragePower - + (this.config.hardwarePowerLimitPerPhase() * DEFAULT_PHASES) - gridPower); + allowedChargePower = (int) (evcsCharge + maxAvailableStoragePower + maxAvailableGridPower); this.logInfoInDebugmode(log, "Calculation of the maximum charge Power: EVCS Charge [" + evcsCharge + "] + Max. available storage power [" + maxAvailableStoragePower @@ -198,34 +214,49 @@ public int getMaximumPowerToDistribute() { allowedChargePower = allowedChargePower > 0 ? allowedChargePower : 0; return allowedChargePower; + } + /** + * Calculate the reduced maximum discharge power. + * + * @param maxEssDischarge original maximum ess discharge power + * @return reduced ess discharge power + */ + private int getSecureEssDischargePower(int maxEssDischarge) { + int soc = this.ess.getSoc().orElse(0); + int startSoc = this.config.ess_secure_discharge_soc(); + int minSoc = this.config.ess_secure_discharge_min_soc(); + double factor = 1.0 / (startSoc - minSoc); + + if (soc >= startSoc) { + return maxEssDischarge; + } + if (soc <= minSoc) { + return (int) (maxEssDischarge * factor); + } + factor = factor * (startSoc - soc); + return (int) (maxEssDischarge - (maxEssDischarge * factor)); } + /** + * Calculates the current grid power depending on the phases if possible. + * + * @return calculated grid power + */ private int getGridPower() { - SymmetricMeter meter; - try { - meter = this.componentManager.getComponent(this.config.meter_id()); - - int gridPower = meter.getActivePower().orElse(0); - - if (meter instanceof AsymmetricMeter) { - AsymmetricMeter asymmetricMeter = (AsymmetricMeter) meter; + int gridPower = this.meter.getActivePower().orElse(0); - int gridPowerL1 = asymmetricMeter.getActivePowerL1().orElse(0); - int gridPowerL2 = asymmetricMeter.getActivePowerL2().orElse(0); - int gridPowerL3 = asymmetricMeter.getActivePowerL3().orElse(0); - - int maxPowerOnPhase = Math.max(Math.max(gridPowerL1, gridPowerL2), gridPowerL3); - gridPower = maxPowerOnPhase * 3; - } + if (this.meter instanceof AsymmetricMeter) { + AsymmetricMeter asymmetricMeter = (AsymmetricMeter) this.meter; - return gridPower; + int gridPowerL1 = asymmetricMeter.getActivePowerL1().orElse(0); + int gridPowerL2 = asymmetricMeter.getActivePowerL2().orElse(0); + int gridPowerL3 = asymmetricMeter.getActivePowerL3().orElse(0); - } catch (OpenemsNamedException e) { - this.logWarn(log, "The component " + this.config.meter_id() - + " is not configured. Maximum power for all EVCS will be calculated without grid power."); - return 0; + int maxPowerOnPhase = Math.max(Math.max(gridPowerL1, gridPowerL2), gridPowerL3); + gridPower = maxPowerOnPhase * 3; } + return gridPower; } @Override diff --git a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java index 8f9f79bdf26..d293c997386 100644 --- a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java +++ b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java @@ -57,7 +57,8 @@ public class EvcsClusterSelfConsumption extends AbstractEvcsCluster implements O public EvcsClusterSelfConsumption() { super(// OpenemsComponent.ChannelId.values(), // - Evcs.ChannelId.values()); + Evcs.ChannelId.values(), // + AbstractEvcsCluster.ChannelId.values()); } @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MULTIPLE) diff --git a/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterPeakShavingTest.java b/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterPeakShavingTest.java deleted file mode 100644 index d36e395850e..00000000000 --- a/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterPeakShavingTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.openems.edge.evcs.cluster; - -import io.openems.common.types.ChannelAddress; -import io.openems.edge.common.sum.DummySum; -import io.openems.edge.common.test.AbstractComponentTest.TestCase; -import io.openems.edge.common.test.ComponentTest; -import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.ess.test.DummyManagedSymmetricEss; -import io.openems.edge.evcs.test.DummyManagedEvcs; -import io.openems.edge.meter.test.DummyAsymmetricMeter; - -/** - * Tried to test that Component without run method. This is currently not - * working. - * - */ -public class EvcsClusterPeakShavingTest { - - private static final String CTRL_ID = "ctrl0"; - - private static final String SUM_ID = "_sum"; - private static final ChannelAddress SUM_ESS_ACTIVE_POWER = new ChannelAddress(SUM_ID, "EssActivePower"); - - private static final String EVCS0_ID = "evcs0"; - private static final ChannelAddress EVCS0_CHARGE_POWER = new ChannelAddress(EVCS0_ID, "ChargePower"); - private static final ChannelAddress EVCS0_SET_CHARGE_POWER_LIMIT = new ChannelAddress(EVCS0_ID, - "SetChargePowerLimit"); - - private static final String EVCS1_ID = "evcs1"; - private static final ChannelAddress EVCS1_CHARGE_POWER = new ChannelAddress(EVCS1_ID, "ChargePower"); - private static final ChannelAddress EVCS1_SET_CHARGE_POWER_LIMIT = new ChannelAddress(EVCS1_ID, - "SetChargePowerLimit"); - - private static final String ESS_ID = "ess0"; - - private static final String METER_ID = "meter0"; - private static final ChannelAddress METER_ACTIVE_POWER = new ChannelAddress(METER_ID, "ActivePower"); - private static final ChannelAddress METER_ACTIVE_POWER_L1 = new ChannelAddress(METER_ID, "ActivePowerL1"); - private static final ChannelAddress METER_ACTIVE_POWER_L2 = new ChannelAddress(METER_ID, "ActivePowerL2"); - private static final ChannelAddress METER_ACTIVE_POWER_L3 = new ChannelAddress(METER_ID, "ActivePowerL3"); - - // private class EvcsClusterComponentTest - // extends AbstractComponentTest - // { - // - // public EvcsClusterComponentTest(AbstractEvcsCluster cluster, - // OpenemsComponent... components) { - // super(cluster); - // for (OpenemsComponent component : components) { - // this.addComponent(component); - // } - // } - // - // @Override - // protected void onAfterControllers() throws OpenemsNamedException { - // /* - // * Warn: Maximum power is not correct, because the evcs power of the whole - // * cluster is still zero - // */ - // this.getSut().getMaximumPowerToDistribute(); - // this.getSut().limitEvcss(); - // } - // - // @Override - // protected EvcsClusterComponentTest self() { - // return this; - // } - // } - - // TODO needs a fix by Sebastian Asen - // @Test - protected void peakShavingClusterTest1() throws Exception { - new ComponentTest(new EvcsClusterPeakShaving()) // - .addReference("componentManager", new DummyComponentManager()) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("sum", new DummySum()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID) // - .withMaxApparentPower(20_000) // - .withAllowedChargePower(20_000) // - .withAllowedDischargePower(20_000)) // - .addReference("addEvcs", new DummyManagedEvcs(EVCS0_ID)) // - .addReference("addEvcs", new DummyManagedEvcs(EVCS1_ID)) // - .addComponent(new DummyAsymmetricMeter(METER_ID)) // - .activate(MyConfigPeakShaving.create() // - .setId(CTRL_ID) // - .setDebugMode(true) // - .setHardwarePowerLimitPerPhase(7360) // - .setEvcsIds(EVCS0_ID, EVCS1_ID).setEssId(ESS_ID) // - .setMeterId(METER_ID) // - .build()) - - /* - * Conditions: - Storage max 60kW - Grid max 22kW i.e. 6kW per phase - Evcss - * 22kW + 3kW - Grid (6|3|3) 12kW - Storage (4,33|4,33|4,33) 13kW - */ - .next(new TestCase() // - .input(SUM_ESS_ACTIVE_POWER, 13000) // - .input(EVCS0_CHARGE_POWER, 22000) // - .input(EVCS1_CHARGE_POWER, 3000) // - .input(METER_ACTIVE_POWER, 12000) // - .input(METER_ACTIVE_POWER_L1, 6000) // - .input(METER_ACTIVE_POWER_L2, 3000) // - .input(METER_ACTIVE_POWER_L3, 3000) // - .output(EVCS0_SET_CHARGE_POWER_LIMIT, 22000) // - .output(EVCS1_SET_CHARGE_POWER_LIMIT, 22000)); - } -} diff --git a/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterTest.java b/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterTest.java new file mode 100644 index 00000000000..7b9b92d9eb0 --- /dev/null +++ b/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/EvcsClusterTest.java @@ -0,0 +1,687 @@ +package io.openems.edge.evcs.cluster; + +import org.junit.Test; + +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.filter.DisabledRampFilter; +import io.openems.edge.common.sum.DummySum; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.ess.test.DummyPower; +import io.openems.edge.evcs.api.Status; +import io.openems.edge.evcs.test.DummyEvcsPower; +import io.openems.edge.evcs.test.DummyManagedEvcs; +import io.openems.edge.meter.test.DummyAsymmetricMeter; + +public class EvcsClusterTest { + // TODO: Add eventually something like DummyEvcsController + + private static final DummyPower POWER = new DummyPower(30000); + private static final DummyManagedSymmetricEss ESS = new DummyManagedSymmetricEss("ess0", POWER); + private static final DummyAsymmetricMeter METER = new DummyAsymmetricMeter("meter0"); + private static final DummyEvcsPower EVCS_POWER = new DummyEvcsPower(new DisabledRampFilter()); + private static final DummyManagedEvcs EVCS0 = new DummyManagedEvcs("evcs0", EVCS_POWER); + private static final DummyManagedEvcs EVCS1 = new DummyManagedEvcs("evcs1", EVCS_POWER); + private static final DummyManagedEvcs EVCS2 = new DummyManagedEvcs("evcs2", EVCS_POWER); + private static final DummyManagedEvcs EVCS3 = new DummyManagedEvcs("evcs3", EVCS_POWER); + private static final DummyManagedEvcs EVCS4 = new DummyManagedEvcs("evcs4", EVCS_POWER); + + private static int HARDWARE_POWER_LIMIT_PER_PHASE = 7000; + private static String EVCS_TARGET; + + private static ChannelAddress sumEssActivePower = new ChannelAddress("_sum", "EssActivePower"); + private static ChannelAddress meterGridActivePower = new ChannelAddress("meter0", "ActivePower"); + private static ChannelAddress meterGridActivePowerL1 = new ChannelAddress("meter0", "ActivePowerL1"); + private static ChannelAddress meterGridActivePowerL2 = new ChannelAddress("meter0", "ActivePowerL2"); + private static ChannelAddress meterGridActivePowerL3 = new ChannelAddress("meter0", "ActivePowerL3"); + private static ChannelAddress essAllowedDischargePower = new ChannelAddress("ess0", "AllowedDischargePower"); + private static ChannelAddress essSoc = new ChannelAddress("ess0", "Soc"); + private static ChannelAddress evcsClusterMaximumPowerToDistribute = new ChannelAddress("evcsCluster0", + "MaximumPowerToDistribute"); + + private static ChannelAddress evcs0Status = new ChannelAddress("evcs0", "Status"); + private static ChannelAddress evcs0ChargePower = new ChannelAddress("evcs0", "ChargePower"); + private static ChannelAddress evcs0MaximumPower = new ChannelAddress("evcs0", "MaximumPower"); + private static ChannelAddress evcs0SetPowerRequest = new ChannelAddress("evcs0", "SetChargePowerRequest"); + private static ChannelAddress evcs0SetChargePowerLimit = new ChannelAddress("evcs0", "SetChargePowerLimit"); + private static ChannelAddress evcs0MaximumHardwarePower = new ChannelAddress("evcs0", "MaximumHardwarePower"); + private static ChannelAddress evcs0MinimumHardwarePower = new ChannelAddress("evcs0", "MinimumHardwarePower"); + + private static ChannelAddress evcs1Status = new ChannelAddress("evcs1", "Status"); + private static ChannelAddress evcs1ChargePower = new ChannelAddress("evcs1", "ChargePower"); + private static ChannelAddress evcs1MaximumPower = new ChannelAddress("evcs1", "MaximumPower"); + private static ChannelAddress evcs1SetPowerRequest = new ChannelAddress("evcs1", "SetChargePowerRequest"); + private static ChannelAddress evcs1SetChargePowerLimit = new ChannelAddress("evcs1", "SetChargePowerLimit"); + private static ChannelAddress evcs1MaximumHardwarePower = new ChannelAddress("evcs1", "MaximumHardwarePower"); + private static ChannelAddress evcs1MinimumHardwarePower = new ChannelAddress("evcs1", "MinimumHardwarePower"); + + private static ChannelAddress evcs2Status = new ChannelAddress("evcs2", "Status"); + private static ChannelAddress evcs2SetPowerRequest = new ChannelAddress("evcs2", "SetChargePowerRequest"); + private static ChannelAddress evcs2SetChargePowerLimit = new ChannelAddress("evcs2", "SetChargePowerLimit"); + + private static ChannelAddress evcs3Status = new ChannelAddress("evcs3", "Status"); + private static ChannelAddress evcs3SetPowerRequest = new ChannelAddress("evcs3", "SetChargePowerRequest"); + private static ChannelAddress evcs3SetChargePowerLimit = new ChannelAddress("evcs3", "SetChargePowerLimit"); + + private static ChannelAddress evcs4Status = new ChannelAddress("evcs4", "Status"); + private static ChannelAddress evcs4SetPowerRequest = new ChannelAddress("evcs4", "SetChargePowerRequest"); + private static ChannelAddress evcs4SetChargePowerLimit = new ChannelAddress("evcs4", "SetChargePowerLimit"); + + @Test + public void clusterMaximum_essActivePowerTest() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, 0) // + .input(meterGridActivePower, 0) // + .input(meterGridActivePowerL1, 0) // + .input(meterGridActivePowerL2, 0) // + .input(meterGridActivePowerL3, 0) // + .input(essAllowedDischargePower, 0) // + .output(evcsClusterMaximumPowerToDistribute, 21000)) // + .next(new TestCase() // + .input(sumEssActivePower, -5000) // + .output(evcsClusterMaximumPowerToDistribute, 26000)) // + .next(new TestCase() // + .input(sumEssActivePower, 6000) // + .output(evcsClusterMaximumPowerToDistribute, 15000)) // + ; + } + + @Test + public void clusterMaximum_symmetricGridPowerTest() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .build()) // + .next(new TestCase() // + .input(meterGridActivePower, -6000) // + .input(meterGridActivePowerL1, -2000) // + .input(meterGridActivePowerL2, -2000) // + .input(meterGridActivePowerL3, -2000) // + .input(essAllowedDischargePower, 0) // + .output(evcsClusterMaximumPowerToDistribute, 27000)) // + .next(new TestCase() // + .input(meterGridActivePower, 4500) // + .input(meterGridActivePowerL1, 1500) // + .input(meterGridActivePowerL2, 1500) // + .input(meterGridActivePowerL3, 1500) // + .input(essAllowedDischargePower, 0) // + .output(evcsClusterMaximumPowerToDistribute, 16500)) // + ; + } + + @Test + public void clusterMaximum_assymmetricGridPowerTest() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .build()) // + .next(new TestCase() // + .input(meterGridActivePower, -4000) // + .input(meterGridActivePowerL1, -2000) // + .input(meterGridActivePowerL2, -1000) // + .input(meterGridActivePowerL3, -1000) // + .input(essAllowedDischargePower, 0) // + .output(evcsClusterMaximumPowerToDistribute, 24000)) // + .next(new TestCase() // + .input(meterGridActivePower, 4500) // + .input(meterGridActivePowerL1, 3000) // + .input(meterGridActivePowerL2, 1500) // + .input(meterGridActivePowerL3, 500) // + .input(essAllowedDischargePower, 0) // + .output(evcsClusterMaximumPowerToDistribute, 12000)) // + ; + } + + @Test + public void clusterMaximum_symmetricGridPower_essActivePowerTest() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .build()) // + .next(new TestCase() // + .input(meterGridActivePower, -6000) // + .input(meterGridActivePowerL1, -2000) // + .input(meterGridActivePowerL2, -2000) // + .input(meterGridActivePowerL3, -2000) // + .input(sumEssActivePower, -6000) // + .input(essAllowedDischargePower, 0) // + .output(evcsClusterMaximumPowerToDistribute, 33000)) // + .next(new TestCase() // + .input(meterGridActivePower, 4500) // + .input(meterGridActivePowerL1, 1500) // + .input(meterGridActivePowerL2, 1500) // + .input(meterGridActivePowerL3, 1500) // + .input(sumEssActivePower, 3000) // + .input(essAllowedDischargePower, 0) // + .output(evcsClusterMaximumPowerToDistribute, 13500)) // + ; + } + + @Test + public void clusterMaximum_essAllowedDischargePowerTest() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .build()) // + .next(new TestCase() // + .input(meterGridActivePower, -6000) // + .input(meterGridActivePowerL1, -2000) // + .input(meterGridActivePowerL2, -2000) // + .input(meterGridActivePowerL3, -2000) // + .input(sumEssActivePower, -6000) // + .input(essAllowedDischargePower, 10000) // + .output(evcsClusterMaximumPowerToDistribute, 43000)) // + .next(new TestCase() // + .input(meterGridActivePower, 4500) // + .input(meterGridActivePowerL1, 1500) // + .input(meterGridActivePowerL2, 1500) // + .input(meterGridActivePowerL3, 1500) // + .input(sumEssActivePower, 3000) // + .input(essAllowedDischargePower, 20000) // + .output(evcsClusterMaximumPowerToDistribute, 33500)) // + ; + } + + @Test + public void clusterDistribution_nothingToChargeTest() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, 0) // + .input(meterGridActivePower, 0) // + .input(meterGridActivePowerL1, 0) // + .input(meterGridActivePowerL2, 0) // + .input(meterGridActivePowerL3, 0) // + .input(essAllowedDischargePower, 0) // + .input(evcs0SetPowerRequest, 0) // + .output(evcs0SetChargePowerLimit, 0)) // + .next(new TestCase() // + .input(sumEssActivePower, 0) // + .input(meterGridActivePower, 21000) // + .input(meterGridActivePowerL1, 7000) // + .input(meterGridActivePowerL2, 7000) // + .input(meterGridActivePowerL3, 7000) // + .input(evcs0SetPowerRequest, 15000) // + .input(essAllowedDischargePower, 0) // + .output(evcsClusterMaximumPowerToDistribute, 0) // + .output(evcs0SetChargePowerLimit, null)) // + .next(new TestCase() // + .input(sumEssActivePower, 0) // + .input(meterGridActivePower, 15000) // + .input(meterGridActivePowerL1, 5000) // + .input(meterGridActivePowerL2, 5000) // + .input(meterGridActivePowerL3, 5000) // + .input(evcs0SetPowerRequest, 15000) // + .input(evcs1SetPowerRequest, 15000) // + .input(evcs0MaximumPower, 22000) // + .input(evcs1MaximumPower, 22000) // + .input(evcs0MinimumHardwarePower, 4500) // + .input(evcs1MinimumHardwarePower, 4500) // + .input(evcs0MaximumHardwarePower, 22000) // + .input(evcs1MaximumHardwarePower, 22000) // + .input(evcs0Status, Status.CHARGING) // + .input(evcs1Status, Status.CHARGING) // + .input(essAllowedDischargePower, 0) // + .output(evcsClusterMaximumPowerToDistribute, 6000) // + .output(evcs0SetChargePowerLimit, 6000) // + .output(evcs1SetChargePowerLimit, 0)) // + ; + } + + @Test + public void clusterDistribution_chargeTest() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1", "evcs2", "evcs3", "evcs4" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, 0) // + .input(meterGridActivePower, 0) // + .input(meterGridActivePowerL1, 0) // + .input(meterGridActivePowerL2, 0) // + .input(meterGridActivePowerL3, 0) // + .input(essAllowedDischargePower, 30000) // + .input(evcs0SetPowerRequest, 15000) // + .input(evcs1SetPowerRequest, 15000) // + .input(evcs2SetPowerRequest, 15000) // + .input(evcs3SetPowerRequest, 15000) // + .input(evcs4SetPowerRequest, 15000) // + .input(evcs0Status, Status.CHARGING) // + .input(evcs1Status, Status.CHARGING) // + .input(evcs2Status, Status.CHARGING) // + .input(evcs3Status, Status.CHARGING) // + .input(evcs4Status, Status.CHARGING) // + .input(evcs0MaximumPower, null) // + .input(evcs1MaximumPower, null) // + .input(evcs0MaximumHardwarePower, 22000) // + .input(evcs1MaximumHardwarePower, 22000) // + .input(evcs0ChargePower, 0) // + .input(evcs1ChargePower, 0)) // + .next(new TestCase() // + .output(evcsClusterMaximumPowerToDistribute, 51000) // + .output(evcs0SetChargePowerLimit, 15000) // + .output(evcs1SetChargePowerLimit, 15000) // + .output(evcs2SetChargePowerLimit, 12000) // + .output(evcs3SetChargePowerLimit, 4500) // + .output(evcs4SetChargePowerLimit, 4500)) // + ; + } + + @Test + public void clusterDistribution_chargeTest2() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, 0) // + .input(meterGridActivePower, 0) // + .input(meterGridActivePowerL1, 0) // + .input(meterGridActivePowerL2, 0) // + .input(meterGridActivePowerL3, 0) // + .input(essAllowedDischargePower, 0) // + .input(evcs0SetPowerRequest, 15000) // + .input(evcs1SetPowerRequest, 15000) // + // TODO: The charge power of an EVCS has to be checked if it really charges + // this amount) + .input(evcs0ChargePower, 11000) // + .input(evcs1ChargePower, 22000) // + .input(evcs0MaximumPower, 22000) // + .input(evcs1MaximumPower, 22000) // + .input(evcs0MaximumHardwarePower, 22000) // + .input(evcs1MaximumHardwarePower, 22000) // + .input(evcs0Status, Status.CHARGING) // + .input(evcs1Status, Status.CHARGING)) // + .next(new TestCase() // + .output(evcsClusterMaximumPowerToDistribute, 54000) // + .output(evcs0SetChargePowerLimit, 15000) // + .output(evcs1SetChargePowerLimit, 15000)) // + ; + } + + @Test + public void clusterDistribution_chargeTest_maximumHardwarePowerTest() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, 0) // + .input(meterGridActivePower, 0) // + .input(meterGridActivePowerL1, 0) // + .input(meterGridActivePowerL2, 0) // + .input(meterGridActivePowerL3, 0) // + .input(essAllowedDischargePower, 0) // + .input(evcs0SetPowerRequest, 15000) // + .input(evcs1SetPowerRequest, 15000) // + .input(evcs0ChargePower, 11000) // + .input(evcs1ChargePower, 0) // + .input(evcs0MaximumPower, null) // + .input(evcs1MaximumPower, null) // + .input(evcs0MaximumHardwarePower, 11000) // + .input(evcs1MaximumHardwarePower, 11000) // + .input(evcs0Status, Status.CHARGING) // + .input(evcs1Status, Status.CHARGING)) // + .next(new TestCase() // + .output(evcsClusterMaximumPowerToDistribute, 32000) // + .output(evcs0SetChargePowerLimit, 11000) // + .output(evcs1SetChargePowerLimit, 11000)) // + ; + } + + @Test + public void clusterDistribution_chargeTest_maximumPowerTest() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, 0) // + .input(meterGridActivePower, 0) // + .input(meterGridActivePowerL1, 0) // + .input(meterGridActivePowerL2, 0) // + .input(meterGridActivePowerL3, 0) // + .input(essAllowedDischargePower, 0) // + .input(evcs0SetPowerRequest, 15000) // + .input(evcs1SetPowerRequest, 15000) // + .input(evcs0ChargePower, 0) // + .input(evcs1ChargePower, 0) // + .input(evcs0MaximumPower, 5000) // + .input(evcs1MaximumPower, 9000) // + .input(evcs0MaximumHardwarePower, 22000) // + .input(evcs1MaximumHardwarePower, 22000) // + .input(evcs0Status, Status.CHARGING) // + .input(evcs1Status, Status.CHARGING)) // + .next(new TestCase() // + .output(evcsClusterMaximumPowerToDistribute, 21000) // + .output(evcs0SetChargePowerLimit, 15000) // + .output(evcs1SetChargePowerLimit, 15000)) // + ; + } + + @Test + public void clusterDistribution_chargeTest_minimumPowerTest() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, 0) // + .input(meterGridActivePower, 0) // + .input(meterGridActivePowerL1, 0) // + .input(meterGridActivePowerL2, 0) // + .input(meterGridActivePowerL3, 0) // + .input(essAllowedDischargePower, 0) // + .input(evcs0SetPowerRequest, 15000) // + .input(evcs1SetPowerRequest, 15000) // + .input(evcs0ChargePower, 0) // + .input(evcs1ChargePower, 0) // + .input(evcs0MaximumPower, 5000) // + .input(evcs1MaximumPower, 9000) // + .input(evcs0MaximumHardwarePower, 22000) // + .input(evcs1MaximumHardwarePower, 22000) // + .input(evcs0MinimumHardwarePower, 4500) // + .input(evcs1MinimumHardwarePower, 4500) // + .input(evcs0Status, Status.READY_FOR_CHARGING) // + .input(evcs1Status, Status.READY_FOR_CHARGING)) // + .next(new TestCase() // + .output(evcsClusterMaximumPowerToDistribute, 21000) // + .output(evcs0SetChargePowerLimit, 4500) // + .output(evcs1SetChargePowerLimit, 4500)) // + .next(new TestCase() // + .input(evcs0MinimumHardwarePower, 9000) // + .input(evcs1MinimumHardwarePower, 6900) // + .output(evcs0SetChargePowerLimit, 9000) // + .output(evcs1SetChargePowerLimit, 6900)) // + ; + } + + + @Test + public void clusterMaximum_secureEssDischargeTest() throws Exception { + String[] EVCS_IDS = { "evcs0", "evcs1" }; + EVCS_TARGET = getEvcsTarget(EVCS_IDS); + + new ComponentTest(new EvcsClusterPeakShaving()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // + .addReference("sum", new DummySum()) // + .addReference("addEvcs", EVCS0) // + .addReference("addEvcs", EVCS1) // + .addReference("addEvcs", EVCS2) // + .addReference("addEvcs", EVCS3) // + .addReference("addEvcs", EVCS4) // + .addReference("meter", METER) // + .addReference("ess", ESS) // + .activate(MyConfigPeakShaving.create() // + .setEssId(ESS.id()) // + .setMeterId(METER.id()) // + .setHardwarePowerLimit(HARDWARE_POWER_LIMIT_PER_PHASE) // + .setEvcsIds(EVCS_IDS) // + .setEvcsTarget(EVCS_TARGET) // + .setEnableSecureEssDischarge(true) // + .setEssSecureDischargeSoc(25) // + .setEssSecureDischargeMinSoc(15) // + .build()) // + .next(new TestCase() // + .input(sumEssActivePower, 0) // + .input(meterGridActivePower, 0) // + .input(meterGridActivePowerL1, 0) // + .input(meterGridActivePowerL2, 0) // + .input(meterGridActivePowerL3, 0) // + .input(essAllowedDischargePower, 20000) // + .input(essSoc, 70) // + .input(evcs0SetPowerRequest, 15000) // + .input(evcs1SetPowerRequest, 15000) // + .input(evcs0ChargePower, 0) // + .input(evcs1ChargePower, 0) // + .input(evcs0MaximumHardwarePower, 22000) // + .input(evcs1MaximumHardwarePower, 22000) // + .input(evcs0MinimumHardwarePower, 4500) // + .input(evcs1MinimumHardwarePower, 4500) // + .input(evcs0Status, Status.READY_FOR_CHARGING) // + .input(evcs1Status, Status.READY_FOR_CHARGING)) // + .next(new TestCase() // + .output(evcsClusterMaximumPowerToDistribute, 41000) // + .output(evcs0SetChargePowerLimit, 4500) // + .output(evcs1SetChargePowerLimit, 4500)) // + .next(new TestCase() // + .input(essSoc, 10) // + .output(evcsClusterMaximumPowerToDistribute, 23000) // + .output(evcs0SetChargePowerLimit, 4500) // + .output(evcs1SetChargePowerLimit, 4500)) // + .next(new TestCase() // + .input(essSoc, null) // + .output(evcsClusterMaximumPowerToDistribute, 23000) // + .output(evcs0SetChargePowerLimit, 4500) // + .output(evcs1SetChargePowerLimit, 4500)) // + .next(new TestCase() // + .input(essSoc, 25) // + .output(evcsClusterMaximumPowerToDistribute, 41000) // + .output(evcs0SetChargePowerLimit, 4500) // + .output(evcs1SetChargePowerLimit, 4500)) // + .next(new TestCase() // + .input(essSoc, 20) // + .output(evcsClusterMaximumPowerToDistribute, 31000) // + .output(evcs0SetChargePowerLimit, 4500) // + .output(evcs1SetChargePowerLimit, 4500)) // + .next(new TestCase() // + .input(essAllowedDischargePower, 50000) // + .input(essSoc, 24) // + .output(evcsClusterMaximumPowerToDistribute, 66000) // + .output(evcs0SetChargePowerLimit, 4500) // + .output(evcs1SetChargePowerLimit, 4500)) // + .next(new TestCase() // + .input(essAllowedDischargePower, 50000) // + .input(essSoc, 16) // + .output(evcsClusterMaximumPowerToDistribute, 26000) // + .output(evcs0SetChargePowerLimit, 4500) // + .output(evcs1SetChargePowerLimit, 4500)) // + ; + } + + + private String getEvcsTarget(String[] evcs_ids) { + StringBuilder stringBuilder = new StringBuilder(); + for (String evcs_id : evcs_ids) { + stringBuilder.append("(id=" + evcs_id + ")"); + } + return "(&(enabled=true)(!(service.pid=evcsCluster0))(|" + stringBuilder.toString() + "))"; + } +} diff --git a/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/MyConfigPeakShaving.java b/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/MyConfigPeakShaving.java index 5da357dbc27..f9a02f0f8bf 100644 --- a/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/MyConfigPeakShaving.java +++ b/io.openems.edge.evcs.cluster/test/io/openems/edge/evcs/cluster/MyConfigPeakShaving.java @@ -1,18 +1,28 @@ package io.openems.edge.evcs.cluster; -import io.openems.common.utils.ConfigUtils; +import java.lang.annotation.Annotation; +import java.util.Arrays; + +import io.openems.edge.common.startstop.StartStopConfig; import io.openems.edge.common.test.AbstractComponentConfig; @SuppressWarnings("all") public class MyConfigPeakShaving extends AbstractComponentConfig implements ConfigPeakShaving { protected static class Builder { - private String id; - private String meterId; - private String essId; - private String[] evcsIds; - private int hardwarePowerLimitPerPhase; - private boolean debugMode; + + private String id = "evcsCluster0"; + private String alias = "Evcs Cluster"; + private boolean enabled = true; + private boolean debugMode = false; + private int hardwarePowerLimitPerPhase = 7000; + private String[] evcs_ids = { "evcs0", "evcs1" }; + private String evcsTarget = "(&(enabled=true)(!(service.pid=evcsCluster0))(|(id=\" + this.evcs_id() + \")))"; + private String ess_id = "ess0"; + private String meter_id = "meter0"; + private int essSecureDischargeSoc = 25; + private int essSecureDischargeMinSoc = 15; + private boolean enableSecureEssDischarge = false; private Builder() { } @@ -22,28 +32,58 @@ public Builder setId(String id) { return this; } + public Builder setAlias(String alias) { + this.alias = alias; + return this; + } + + public Builder setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + public Builder setDebugMode(boolean debugMode) { this.debugMode = debugMode; return this; } - public Builder setEssId(String essId) { - this.essId = essId; + public Builder setHardwarePowerLimit(int hardwarePowerLimitPerPhase) { + this.hardwarePowerLimitPerPhase = hardwarePowerLimitPerPhase; return this; } - public Builder setHardwarePowerLimitPerPhase(int hardwarePowerLimitPerPhase) { - this.hardwarePowerLimitPerPhase = hardwarePowerLimitPerPhase; + public Builder setEvcsIds(String[] evcs_ids) { + this.evcs_ids = evcs_ids; return this; } - public Builder setEvcsIds(String... evcsIds) { - this.evcsIds = evcsIds; + public Builder setEvcsTarget(String evcsTarget) { + this.evcsTarget = evcsTarget; return this; } - public Builder setMeterId(String meterId) { - this.meterId = meterId; + public Builder setEssId(String ess_id) { + this.ess_id = ess_id; + return this; + } + + public Builder setEssSecureDischargeSoc(int essSecureDischargeSoc) { + this.essSecureDischargeSoc = essSecureDischargeSoc; + return this; + } + + public Builder setEssSecureDischargeMinSoc(int essSecureDischargeMinSoc) { + this.essSecureDischargeMinSoc = essSecureDischargeMinSoc; + return this; + } + + public Builder setEnableSecureEssDischarge(boolean enableSecureEssDischarge) { + this.enableSecureEssDischarge = enableSecureEssDischarge; + return this; + } + + public Builder setMeterId(String meter_id) { + this.meter_id = meter_id; return this; } @@ -52,11 +92,6 @@ public MyConfigPeakShaving build() { } } - /** - * Create a Config builder. - * - * @return a {@link Builder} - */ public static Builder create() { return new Builder(); } @@ -68,34 +103,53 @@ private MyConfigPeakShaving(Builder builder) { this.builder = builder; } + @Override + public String id() { + return this.builder.id; + } + @Override public boolean debugMode() { return this.builder.debugMode; } @Override - public int hardwarePowerLimitPerPhase() { - return this.builder.hardwarePowerLimitPerPhase; + public String[] evcs_ids() { + return this.builder.evcs_ids; } @Override - public String[] evcs_ids() { - return this.builder.evcsIds; + public String ess_id() { + return this.builder.ess_id; } @Override public String Evcs_target() { - return ConfigUtils.generateReferenceTargetFilter(this.id(), this.evcs_ids()); + return this.builder.evcsTarget; } @Override - public String ess_id() { - return this.builder.essId; + public int hardwarePowerLimitPerPhase() { + return this.builder.hardwarePowerLimitPerPhase; } @Override public String meter_id() { - return this.builder.meterId; + return this.builder.meter_id; + } + + @Override + public boolean enable_secure_ess_discharge() { + return this.builder.enableSecureEssDischarge; } + @Override + public int ess_secure_discharge_soc() { + return this.builder.essSecureDischargeSoc; + } + + @Override + public int ess_secure_discharge_min_soc() { + return this.builder.essSecureDischargeMinSoc; + } } diff --git a/io.openems.edge.evcs.core/.classpath b/io.openems.edge.evcs.core/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/io.openems.edge.evcs.core/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/io.openems.edge.evcs.core/.gitignore b/io.openems.edge.evcs.core/.gitignore new file mode 100644 index 00000000000..c2b941a96de --- /dev/null +++ b/io.openems.edge.evcs.core/.gitignore @@ -0,0 +1,2 @@ +/bin_test/ +/generated/ diff --git a/io.openems.edge.evcs.core/.project b/io.openems.edge.evcs.core/.project new file mode 100644 index 00000000000..c69dce6a9b4 --- /dev/null +++ b/io.openems.edge.evcs.core/.project @@ -0,0 +1,23 @@ + + + io.openems.edge.evcs.core + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.edge.evcs.core/bnd.bnd b/io.openems.edge.evcs.core/bnd.bnd new file mode 100644 index 00000000000..10361a88f2c --- /dev/null +++ b/io.openems.edge.evcs.core/bnd.bnd @@ -0,0 +1,14 @@ +Bundle-Name: OpenEMS Edge EVCS Core +Bundle-Vendor: FENECON GmbH +Bundle-License: https://opensource.org/licenses/EPL-2.0 +Bundle-Version: 1.0.0.${tstamp} + +-buildpath: \ + ${buildpath},\ + io.openems.common,\ + io.openems.edge.common,\ + io.openems.edge.ess.core,\ + io.openems.edge.evcs.api + +-testpath: \ + ${testpath} diff --git a/io.openems.edge.evcs.core/readme.adoc b/io.openems.edge.evcs.core/readme.adoc new file mode 100644 index 00000000000..b86f2ab525f --- /dev/null +++ b/io.openems.edge.evcs.core/readme.adoc @@ -0,0 +1,5 @@ += EVCS Core + +Core services for electric vehicle charging. + +https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.evcs.core[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.evcs.core/src/io/openems/edge/evcs/core/Config.java b/io.openems.edge.evcs.core/src/io/openems/edge/evcs/core/Config.java new file mode 100644 index 00000000000..9ecbdac54c9 --- /dev/null +++ b/io.openems.edge.evcs.core/src/io/openems/edge/evcs/core/Config.java @@ -0,0 +1,21 @@ +package io.openems.edge.evcs.core; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +import io.openems.edge.common.filter.RampFilter; + +@ObjectClassDefinition(// + name = "EVCS Power", // + description = "This component defines the increase rate for the ramp filter for every EVCS Component.") +@interface Config { + + @AttributeDefinition(name = "Enable Slow Power Increase Filter", description = "Enables the Slow Power Increase Filter with the settings for the increasing rate below") + boolean enableSlowIncrease() default false; + + @AttributeDefinition(name = "Rate of increase", description = "The rate of increase between 0 and 1.") + double increaseRate() default RampFilter.DEFAULT_INCREASE_RATE; + + String webconsole_configurationFactory_nameHint() default "EVCS Slow Power Increase Filter"; + +} diff --git a/io.openems.edge.evcs.core/src/io/openems/edge/evcs/core/EvcsPowerComponent.java b/io.openems.edge.evcs.core/src/io/openems/edge/evcs/core/EvcsPowerComponent.java new file mode 100644 index 00000000000..785dd226082 --- /dev/null +++ b/io.openems.edge.evcs.core/src/io/openems/edge/evcs/core/EvcsPowerComponent.java @@ -0,0 +1,73 @@ +package io.openems.edge.evcs.core; + +import java.util.Map; + +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.Modified; +import org.osgi.service.event.EventConstants; +import org.osgi.service.metatype.annotations.Designate; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.event.EdgeEventConstants; +import io.openems.edge.common.filter.DisabledRampFilter; +import io.openems.edge.common.filter.RampFilter; +import io.openems.edge.evcs.api.EvcsPower; + +@Designate(ocd = Config.class, factory = false) +@Component(// + name = "Evcs.SlowPowerIncreaseFilter", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.OPTIONAL, // + property = { // + "id=_evcsSlowPowerIncreaseFilter", // + "enabled=true", // + EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_BEFORE_WRITE, // + EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE // + }) +public class EvcsPowerComponent extends AbstractOpenemsComponent implements OpenemsComponent, EvcsPower { + + private double increasingRate = RampFilter.DEFAULT_INCREASE_RATE; + private RampFilter rampFilter; + + public EvcsPowerComponent() { + super(// + OpenemsComponent.ChannelId.values() // + ); + } + + @Activate + void activate(ComponentContext context, Map properties, Config config) { + super.activate(context, "_evcsSlowPowerIncreaseFilter", "Evcs.SlowPowerIncreaseFilter", true); + this.updateConfig(config); + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Modified + void modified(ComponentContext context, Config config) throws OpenemsNamedException { + super.activate(context, "_evcsSlowPowerIncreaseFilter", "Evcs.SlowPowerIncreaseFilter", true); + this.updateConfig(config); + } + + private void updateConfig(Config config) { + if (config.enableSlowIncrease()) { + this.rampFilter = new RampFilter(this.increasingRate); + } else { + this.rampFilter = new DisabledRampFilter(); + } + } + + @Override + public RampFilter getRampFilter() { + return this.rampFilter; + } +} diff --git a/io.openems.edge.evcs.core/test/.gitignore b/io.openems.edge.evcs.core/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/KebaKeContact.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/KebaKeContact.java index 0d8c59b5d96..db7b4130011 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/KebaKeContact.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/KebaKeContact.java @@ -32,8 +32,9 @@ import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.modbusslave.ModbusType; -import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.Evcs; +import io.openems.edge.evcs.api.EvcsPower; +import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.keba.kecontact.core.KebaKeContactCore; @Designate(ocd = Config.class, factory = true) @@ -54,6 +55,9 @@ public class KebaKeContact extends AbstractOpenemsComponent protected Config config; + @Reference + private EvcsPower evcsPower; + @Reference(policy = ReferencePolicy.STATIC, cardinality = ReferenceCardinality.MANDATORY) private KebaKeContactCore kebaKeContactCore = null; @@ -242,4 +246,9 @@ private ModbusSlaveNatureTable getModbusSlaveNatureTable(AccessMode accessMode) .channel(86, KebaChannelId.COS_PHI, ModbusType.UINT16).uint16Reserved(87) .channel(88, KebaChannelId.ENERGY_TOTAL, ModbusType.UINT16).build(); } + + @Override + public EvcsPower getEvcsPower() { + return this.evcsPower; + } } diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java index 36b8bbb7fff..3c5c65b3ccd 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadHandler.java @@ -82,10 +82,9 @@ public void accept(String message) { receiveReport2 = true; setInt(KebaChannelId.STATUS_KEBA, jsonMessage, "State"); - // The setenergy value of KEBA is not used because it is reset by the currtime 0 - // 1 command + // Value "setenergy" not used, because it is reset by the currtime 0 1 command - // Set STATUS and Warning STATE Channel + // Set Evcs status Channel stateChannel = this.parent.channel(KebaChannelId.STATUS_KEBA); Channel plugChannel = this.parent.channel(KebaChannelId.PLUG); @@ -128,7 +127,6 @@ public void accept(String message) { int energy = this.parent.getEnergySession().orElse(0); if (energy >= limit && limit != 0) { try { - this.parent.setDisplayText(limit + "Wh erreicht"); status = Status.ENERGY_LIMIT_REACHED; } catch (OpenemsNamedException e) { @@ -136,13 +134,9 @@ public void accept(String message) { } } - this.parent.channel(Evcs.ChannelId.STATUS).setNextValue(status); - - if (status == Status.ERROR) { - this.parent.channel(KebaChannelId.CHARGINGSTATION_STATE_ERROR).setNextValue(true); - } else { - this.parent.channel(KebaChannelId.CHARGINGSTATION_STATE_ERROR).setNextValue(false); - } + this.parent._setStatus(status); + boolean errorState = status == Status.ERROR ? true : false; + this.parent.channel(KebaChannelId.CHARGINGSTATION_STATE_ERROR).setNextValue(errorState); setInt(KebaChannelId.ERROR_1, jsonMessage, "Error1"); setInt(KebaChannelId.ERROR_2, jsonMessage, "Error2"); @@ -183,17 +177,17 @@ public void accept(String message) { int currentSum = currentL1.value().orElse(0) + currentL2.value().orElse(0) + currentL3.value().orElse(0); - if (currentSum > 100) { + if (currentSum > 300) { int phases = 0; - if (currentL1.value().orElse(0) > 100) { + if (currentL1.value().orElse(0) >= 100) { phases += 1; } - if (currentL2.value().orElse(0) > 100) { + if (currentL2.value().orElse(0) >= 100) { phases += 1; } - if (currentL3.value().orElse(0) > 100) { + if (currentL3.value().orElse(0) >= 100) { phases += 1; } this.parent._setPhases(phases); diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java index 606219fd414..38135bb643a 100644 --- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java +++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/ReadWorker.java @@ -16,6 +16,7 @@ public class ReadWorker extends AbstractWorker { private boolean validateReport1 = false; private boolean validateReport2 = false; private boolean validateReport3 = false; + private static int MAX_TIME_TILL_REPLY = 15; // sec public ReadWorker(KebaKeContact parent) { this.parent = parent; @@ -60,16 +61,16 @@ protected void forever() throws InterruptedException { // RESULTS // Sets the state of the component if the report doesn't answer in a few seconds - if (this.validateReport1 && this.lastReport1.isBefore(LocalDateTime.now().minusSeconds(2))) { + if (this.validateReport1 && this.lastReport1.isBefore(LocalDateTime.now().minusSeconds(MAX_TIME_TILL_REPLY))) { currentCommunication(this.parent.getReadHandler().hasResultandReset(Report.REPORT1)); this.validateReport1 = false; } - if (this.validateReport2 && this.lastReport2.isBefore(LocalDateTime.now().minusSeconds(2))) { + if (this.validateReport2 && this.lastReport2.isBefore(LocalDateTime.now().minusSeconds(MAX_TIME_TILL_REPLY))) { currentCommunication(this.parent.getReadHandler().hasResultandReset(Report.REPORT2)); this.validateReport2 = false; } - if (this.validateReport3 && this.lastReport3.isBefore(LocalDateTime.now().minusSeconds(2))) { + if (this.validateReport3 && this.lastReport3.isBefore(LocalDateTime.now().minusSeconds(MAX_TIME_TILL_REPLY))) { currentCommunication(this.parent.getReadHandler().hasResultandReset(Report.REPORT3)); this.validateReport3 = false; } diff --git a/io.openems.edge.evcs.ocpp.abl/bnd.bnd b/io.openems.edge.evcs.ocpp.abl/bnd.bnd index 6c92501b696..fa5e4962570 100644 --- a/io.openems.edge.evcs.ocpp.abl/bnd.bnd +++ b/io.openems.edge.evcs.ocpp.abl/bnd.bnd @@ -11,6 +11,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.evcs.ocpp.common,\ io.openems.edge.evcs.ocpp.server,\ io.openems.edge.meter.api,\ + io.openems.edge.timedata.api,\ io.openems.wrapper.eu.chargetime.ocpp,\ -testpath: \ diff --git a/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/Abl.java b/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/Abl.java index 7bcecf30edf..7f05a323ad9 100644 --- a/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/Abl.java +++ b/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/Abl.java @@ -15,12 +15,7 @@ import org.osgi.service.event.EventConstants; import org.osgi.service.event.EventHandler; import org.osgi.service.metatype.annotations.Designate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import eu.chargetime.ocpp.NotConnectedException; -import eu.chargetime.ocpp.OccurenceConstraintException; -import eu.chargetime.ocpp.UnsupportedFeatureException; import eu.chargetime.ocpp.model.Request; import eu.chargetime.ocpp.model.core.ChangeConfigurationRequest; import eu.chargetime.ocpp.model.core.DataTransferRequest; @@ -31,6 +26,7 @@ import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.evcs.api.ChargingType; import io.openems.edge.evcs.api.Evcs; +import io.openems.edge.evcs.api.EvcsPower; import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.MeasuringEvcs; import io.openems.edge.evcs.ocpp.common.AbstractOcppEvcsComponent; @@ -43,12 +39,13 @@ name = "Evcs.Ocpp.Abl", // immediate = true, // configurationPolicy = ConfigurationPolicy.REQUIRE, // - property = EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE) + property = { // + EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE, // + EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE // + }) public class Abl extends AbstractOcppEvcsComponent implements Evcs, MeasuringEvcs, ManagedEvcs, OpenemsComponent, EventHandler { - private final Logger log = LoggerFactory.getLogger(Abl.class); - // Default value for the hardware limit private static final Integer DEFAULT_HARDWARE_LIMIT = 22080; @@ -69,6 +66,9 @@ public class Abl extends AbstractOcppEvcsComponent private Config config; + @Reference + private EvcsPower evcsPower; + @Reference protected ComponentManager componentManager; @@ -105,37 +105,16 @@ public Integer getConfiguredConnectorId() { return this.config.connectorId(); } - private int dynamicMaximumHardwarePower = 0; - @Override public Integer getConfiguredMaximumHardwarePower() { - if (this.sessionId == null || this.ocppServer == null) { - return this.config.maxHwPower(); - } - DataTransferRequest request = new DataTransferRequest("ABL"); - request.setMessageId("GetLimit"); - request.setData(this.config.limitId()); - - try { - this.ocppServer.send(this.sessionId, request).whenComplete((confirmation, throwable) -> { - - dynamicMaximumHardwarePower = Integer.valueOf(confirmation.toString()); - this.logInfo(log, confirmation.toString()); - }); - return dynamicMaximumHardwarePower; - } catch (OccurenceConstraintException e) { - e.printStackTrace(); - } catch (UnsupportedFeatureException e) { - e.printStackTrace(); - } catch (NotConnectedException e) { - e.printStackTrace(); - } - return this.config.maxHwPower(); + // TODO: Set dynamically. Problem: No phases calculation possible. + return (int) (this.config.maxHwCurrent() / 1000.0) * 230 * 3; } @Override public Integer getConfiguredMinimumHardwarePower() { - return this.config.minHwPower(); + // TODO: Set dynamically. Problem: No phases calculation possible. + return (int) (this.config.minHwCurrent() / 1000.0) * 230 * 3; } @Override @@ -177,7 +156,7 @@ public List getRequiredRequestsAfterConnection() { requests.add(setMeterValueSampleInterval); ChangeConfigurationRequest setMeterValueSampledData = new ChangeConfigurationRequest("MeterValuesSampledData", - "Energy.Active.Import.Register,Current.Import,Voltage,Power.Active.Import,"); + "Energy.Active.Import.Register,Current.Import,Voltage,Power.Active.Import,Temperature"); requests.add(setMeterValueSampledData); return requests; @@ -201,4 +180,14 @@ public List getRequiredRequestsDuringConnection() { return requests; } + + @Override + public EvcsPower getEvcsPower() { + return this.evcsPower; + } + + @Override + public boolean returnsSessionEnergy() { + return false; + } } diff --git a/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/Config.java b/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/Config.java index fc53d3e546d..c35c15fd13a 100644 --- a/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/Config.java +++ b/io.openems.edge.evcs.ocpp.abl/src/io/openems/edge/evcs/ocpp/abl/Config.java @@ -29,11 +29,11 @@ @AttributeDefinition(name = "ABL limit identifier", description = "The limit id defined in the web administration interface of the ABL chargepoint.", required = true) String limitId() default "limit100"; - @AttributeDefinition(name = "Maximum power", description = "Maximum power of the charger in Watt.", required = true) - int maxHwPower() default 22080; + @AttributeDefinition(name = "Maximum current", description = "Maximum current of the charger in mA.", required = true) + int maxHwCurrent() default 32000; - @AttributeDefinition(name = "Minimum power", description = "Minimum power of the charger in Watt.", required = true) - int minHwPower() default 0; + @AttributeDefinition(name = "Minimum current", description = "Minimum current of the Charger in mA.", required = true) + int minHwCurrent() default 6000; String webconsole_configurationFactory_nameHint() default "EVCS OCPP ABL [{id}]"; } diff --git a/io.openems.edge.evcs.ocpp.common/bnd.bnd b/io.openems.edge.evcs.ocpp.common/bnd.bnd index d9ba5291d98..9fb78299fe5 100644 --- a/io.openems.edge.evcs.ocpp.common/bnd.bnd +++ b/io.openems.edge.evcs.ocpp.common/bnd.bnd @@ -9,7 +9,8 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.common,\ io.openems.edge.evcs.api,\ io.openems.edge.meter.api,\ + io.openems.edge.timedata.api,\ io.openems.wrapper.eu.chargetime.ocpp,\ - + -testpath: \ ${testpath} diff --git a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/AbstractOcppEvcsComponent.java b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/AbstractOcppEvcsComponent.java index 1a975300966..537a8275080 100644 --- a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/AbstractOcppEvcsComponent.java +++ b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/AbstractOcppEvcsComponent.java @@ -7,22 +7,31 @@ import java.util.UUID; import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.event.Event; import org.osgi.service.event.EventHandler; import org.slf4j.Logger; import eu.chargetime.ocpp.model.Request; +import io.openems.common.types.ChannelAddress; +import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; +import io.openems.edge.common.type.TypeUtils; import io.openems.edge.evcs.api.Evcs; import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.MeasuringEvcs; import io.openems.edge.evcs.api.Status; +import io.openems.edge.timedata.api.Timedata; +import io.openems.edge.timedata.api.TimedataProvider; public abstract class AbstractOcppEvcsComponent extends AbstractOpenemsComponent - implements Evcs, MeasuringEvcs, EventHandler { + implements Evcs, MeasuringEvcs, EventHandler, TimedataProvider { private ChargingProperty lastChargingProperty = null; @@ -34,6 +43,13 @@ public abstract class AbstractOcppEvcsComponent extends AbstractOpenemsComponent protected UUID sessionId = null; + private ChargeSessionStamp sessionStart = new ChargeSessionStamp(); + + private ChargeSessionStamp sessionEnd = new ChargeSessionStamp(); + + @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) + private volatile Timedata timedata = null; + protected AbstractOcppEvcsComponent(OcppProfileType[] profileTypes, io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds, io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) { @@ -62,24 +78,60 @@ protected void activate(ComponentContext context, String id, String alias, boole this.channel(Evcs.ChannelId.MAXIMUM_HARDWARE_POWER).setNextValue(getConfiguredMaximumHardwarePower()); this.channel(Evcs.ChannelId.MINIMUM_HARDWARE_POWER).setNextValue(getConfiguredMinimumHardwarePower()); - this._setEnergySession(0); } @Override public void handleEvent(Event event) { switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE: + this.setInitialTotalEnergyFromTimedata(); + break; + case EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE: if (this.sessionId == null) { + lostSession(); return; } - if (this.getStatus().equals(Status.CHARGING_FINISHED)) { - this.resetMeasuredChannelValues(); - } - writeHandler.run(); + + this.checkCurrentState(); + this.writeHandler.run(); break; } } + /** + * Initialize total energy value from from Timedata service if it is not already + * set. + */ + private void setInitialTotalEnergyFromTimedata() { + Long totalEnergy = this.getActiveConsumptionEnergy().orElse(null); + + // Total energy already set + if (totalEnergy != null) { + return; + } + + Timedata timedata = this.getTimedata(); + String componentId = this.id(); + if (timedata == null || componentId == null) { + return; + } else { + timedata.getLatestValue(new ChannelAddress(componentId, "ActiveConstumptionEnergy")) + .thenAccept(totalEnergyOpt -> { + + if (totalEnergyOpt.isPresent()) { + try { + this._setActiveConsumptionEnergy(TypeUtils.getAsType(OpenemsType.LONG, totalEnergyOpt.get())); + } catch (IllegalArgumentException e) { + this._setActiveConsumptionEnergy(TypeUtils.getAsType(OpenemsType.LONG, 0L)); + } + } else { + this._setActiveConsumptionEnergy(TypeUtils.getAsType(OpenemsType.LONG, 0L)); + } + }); + } + } + @Override protected void deactivate() { super.deactivate(); @@ -109,6 +161,8 @@ public void lostSession() { public abstract Integer getConfiguredMinimumHardwarePower(); + public abstract boolean returnsSessionEnergy(); + /** * Required requests that should be sent after a connection was established. * @@ -134,6 +188,9 @@ public UUID getSessionId() { return this.sessionId; }; + /** + * Reset the measured channel values and the charge power. + */ private void resetMeasuredChannelValues() { for (MeasuringEvcs.ChannelId c : MeasuringEvcs.ChannelId.values()) { Channel channel = this.channel(c); @@ -142,6 +199,29 @@ private void resetMeasuredChannelValues() { this._setChargePower(0); } + /** + * Check the current state and resets the measured values. + */ + private void checkCurrentState() { + Status state = this.getStatus(); + switch (state) { + case CHARGING: + break; + case CHARGING_FINISHED: + this.resetMeasuredChannelValues(); + break; + case CHARGING_REJECTED: + case ENERGY_LIMIT_REACHED: + case ERROR: + case NOT_READY_FOR_CHARGING: + case READY_FOR_CHARGING: + case STARTING: + case UNDEFINED: + this._setChargePower(0); + break; + } + } + public ChargingProperty getLastChargingProperty() { return this.lastChargingProperty; } @@ -150,6 +230,19 @@ public void setLastChargingProperty(ChargingProperty chargingProperty) { this.lastChargingProperty = chargingProperty; } + public ChargeSessionStamp getSessionStart() { + return sessionStart; + } + + public ChargeSessionStamp getSessionEnd() { + return sessionEnd; + } + + @Override + public Timedata getTimedata() { + return this.timedata; + } + @Override protected void logInfo(Logger log, String message) { super.logInfo(log, message); diff --git a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/ChargeSessionStamp.java b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/ChargeSessionStamp.java new file mode 100644 index 00000000000..5729f944255 --- /dev/null +++ b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/ChargeSessionStamp.java @@ -0,0 +1,82 @@ +package io.openems.edge.evcs.ocpp.common; + +import java.time.Instant; + +public class ChargeSessionStamp { + + private Instant time; + private long energy; + + /** + * Constructor of a ChargeSession with the given time and energy. + */ + public ChargeSessionStamp(Instant time, long energy) { + this.time = time; + this.energy = energy; + } + + /** + * Constructor of a ChargeSession with the initial Energy. + *

+ * The time will be initialized by Instant.now(). + */ + public ChargeSessionStamp(long energy) { + this(Instant.now(), energy); + } + + /** + * Constructor of a ChargeSession with the initial Time. + *

+ * The energy will be initialized by 0. + */ + public ChargeSessionStamp(Instant time) { + this(time, 0); + } + + /** + * Constructor of a ChargeSession with no initial values. + */ + public ChargeSessionStamp() { + this(null, 0); + } + + public Instant getTime() { + return time; + } + + public void setTime(Instant time) { + this.time = time; + } + + public long getEnergy() { + return energy; + } + + public void setEnergy(long energy) { + this.energy = energy; + } + + public boolean isChargeSessionStampPresent() { + return this.time != null; + } + + public void setChargeSessionStamp(Instant time, long energy) { + this.time = time; + this.energy = energy; + } + + public void resetChargeSessionStamp() { + this.time = null; + this.energy = 0; + } + + public void setChargeSessionStampIfNotPresent(Instant time, long energy) { + this.setChargeSessionStamp(time, energy); + } + + public void resetChargeSessionStampIfPresent() { + if(isChargeSessionStampPresent()) { + this.resetChargeSessionStamp(); + } + } +} diff --git a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/bnd.bnd b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/bnd.bnd index 2acfeb62304..92278c8f5dc 100644 --- a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/bnd.bnd +++ b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/bnd.bnd @@ -11,7 +11,8 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.evcs.ocpp.common,\ io.openems.edge.evcs.ocpp.server,\ io.openems.edge.meter.api,\ + io.openems.edge.timedata.api,\ io.openems.wrapper.eu.chargetime.ocpp,\ - + -testpath: \ ${testpath} diff --git a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/Config.java b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/Config.java index 52cdcd6b96f..be5bc5ed10f 100644 --- a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/Config.java +++ b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/Config.java @@ -18,7 +18,7 @@ boolean enabled() default true; @AttributeDefinition(name = "OCPP chargepoint identifier", description = "The OCPP identifier of the charging station.", required = true) - String ocpp_id() default ""; + String ocpp_id() default "IES1"; @AttributeDefinition(name = "OCPP connector identifier", description = "The connector id of the chargepoint (e.g. if there are two connectors, then the evcs has two id's 1 and 2).", required = true) int connectorId() default 1; diff --git a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/IesKeywattSingleCcs.java b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/IesKeywattSingleCcs.java index 3b5db2eb1ca..9694d053ca1 100644 --- a/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/IesKeywattSingleCcs.java +++ b/io.openems.edge.evcs.ocpp.ies.keywatt.singleccs/src/io/openems/edge/evcs/ocpp/ies/keywatt/singleccs/IesKeywattSingleCcs.java @@ -25,6 +25,7 @@ import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.evcs.api.ChargingType; import io.openems.edge.evcs.api.Evcs; +import io.openems.edge.evcs.api.EvcsPower; import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.MeasuringEvcs; import io.openems.edge.evcs.ocpp.common.AbstractOcppEvcsComponent; @@ -37,7 +38,10 @@ name = "Evcs.Ocpp.IesKeywattSingle", // immediate = true, // configurationPolicy = ConfigurationPolicy.REQUIRE, // - property = EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE) + property = { // + EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE, // + EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE // + }) public class IesKeywattSingleCcs extends AbstractOcppEvcsComponent implements Evcs, ManagedEvcs, MeasuringEvcs, OpenemsComponent, EventHandler { @@ -53,6 +57,9 @@ public class IesKeywattSingleCcs extends AbstractOcppEvcsComponent private Config config; + @Reference + private EvcsPower evcsPower; + @Reference protected ComponentManager componentManager; @@ -135,4 +142,15 @@ public List getRequiredRequestsAfterConnection() { public List getRequiredRequestsDuringConnection() { return new ArrayList(); } + + @Override + public EvcsPower getEvcsPower() { + return this.evcsPower; + } + + @Override + public boolean returnsSessionEnergy() { + return true; + } + } diff --git a/io.openems.edge.evcs.ocpp.server/bnd.bnd b/io.openems.edge.evcs.ocpp.server/bnd.bnd index 6356b80dae5..a049b7a19dd 100644 --- a/io.openems.edge.evcs.ocpp.server/bnd.bnd +++ b/io.openems.edge.evcs.ocpp.server/bnd.bnd @@ -9,6 +9,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.common,\ io.openems.edge.evcs.api,\ io.openems.edge.evcs.ocpp.common,\ + io.openems.edge.timedata.api,\ io.openems.wrapper.eu.chargetime.ocpp,\ Java-WebSocket,\ diff --git a/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/CoreEventHandlerImpl.java b/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/CoreEventHandlerImpl.java index cee514e34ee..879743006d4 100644 --- a/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/CoreEventHandlerImpl.java +++ b/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/CoreEventHandlerImpl.java @@ -1,9 +1,11 @@ package io.openems.edge.evcs.ocpp.server; import java.time.Duration; +import java.time.Instant; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.slf4j.Logger; @@ -34,7 +36,6 @@ import eu.chargetime.ocpp.model.core.StopTransactionConfirmation; import eu.chargetime.ocpp.model.core.StopTransactionRequest; import eu.chargetime.ocpp.model.core.ValueFormat; -import io.openems.edge.evcs.api.MeasuringEvcs; import io.openems.edge.evcs.api.Status; import io.openems.edge.evcs.ocpp.common.AbstractOcppEvcsComponent; import io.openems.edge.evcs.ocpp.common.ChargingProperty; @@ -99,8 +100,6 @@ public MeterValuesConfirmation handleMeterValuesRequest(UUID sessionIndex, Meter return new MeterValuesConfirmation(); } - evcs._setStatus(Status.CHARGING); - /* * Set the channels depending on the meter values */ @@ -161,9 +160,30 @@ public MeterValuesConfirmation handleMeterValuesRequest(UUID sessionIndex, Meter if (!evcs.getSupportedMeasurements() .contains(OcppInformations.CORE_METER_VALUES_POWER_ACTIVE_IMPORT)) { this.setPowerDependingOnEnergy(evcs, (Double) correctValue, meterValue.getTimestamp()); + // TODO: Currently not working with session energy values + } + + long energy = (long) Math.round((Double) correctValue); + if (!evcs.getSessionStart().isChargeSessionStampPresent()) { + break; + } + + int sessionEnergy = 0; + long totalEnergy = 0; + + /* + * Calculating the energy in this session and in total for the given energy + * value. + */ + if (evcs.returnsSessionEnergy()) { + sessionEnergy = (int) energy; + totalEnergy = evcs.getSessionStart().getEnergy() + energy; + } else { + sessionEnergy = (int) (energy - evcs.getSessionStart().getEnergy()); + totalEnergy = energy; } - // Some Evcss responding the session Energy and other the total Energy. - evcs._setEnergySession((int) Math.round((Double) correctValue)); + evcs._setEnergySession(sessionEnergy); + evcs._setActiveConsumptionEnergy(totalEnergy); break; case CORE_METER_VALUES_ENERGY_REACTIVE_EXPORT_REGISTER: @@ -183,6 +203,27 @@ public MeterValuesConfirmation handleMeterValuesRequest(UUID sessionIndex, Meter val = this.multipliedByThousand(val); } correctValue = (int) Math.round(Double.valueOf(val)); + + /* + * Sets the start and end session stamp depending on the the current power. + */ + Instant now = Instant.now(this.parent.componentManager.getClock()); + + if ((int) correctValue > 0) { + evcs._setStatus(Status.CHARGING); + } + + // Has to provide a not null energy value + Optional currEnergy = evcs.getActiveConsumptionEnergy().asOptional(); + if (currEnergy.isPresent()) { + if ((int) correctValue > 0) { + evcs.getSessionStart().setChargeSessionStampIfNotPresent(now, currEnergy.get()); + evcs.getSessionEnd().resetChargeSessionStampIfPresent(); + } else { + evcs.getSessionStart().resetChargeSessionStampIfPresent(); + evcs.getSessionEnd().setChargeSessionStampIfNotPresent(now, currEnergy.get()); + } + } break; case CORE_METER_VALUES_POWER_REACTIVE_EXPORT: @@ -217,7 +258,7 @@ public StatusNotificationConfirmation handleStatusNotificationRequest(UUID sessi StatusNotificationRequest request) { this.logDebug("Handle StatusNotificationRequest: " + request); - MeasuringEvcs evcs = this.getEvcsBySessionIndexAndConnector(sessionIndex, request.getConnectorId()); + AbstractOcppEvcsComponent evcs = this.getEvcsBySessionIndexAndConnector(sessionIndex, request.getConnectorId()); if (evcs == null) { return new StatusNotificationConfirmation(); } @@ -234,12 +275,25 @@ public StatusNotificationConfirmation handleStatusNotificationRequest(UUID sessi break; case Charging: evcsStatus = Status.CHARGING; + + // Reset the end charge session stamp + evcs.getSessionEnd().resetChargeSessionStampIfPresent(); + + // Set the start charge session stamp + evcs.getSessionStart().setChargeSessionStampIfNotPresent( + Instant.now(this.parent.componentManager.getClock()), evcs.getActiveConsumptionEnergy().orElse(0L)); break; case Faulted: evcsStatus = Status.ERROR; break; case Finishing: evcsStatus = Status.CHARGING_FINISHED; + + // Reset the start charge session stamp + evcs.getSessionStart().resetChargeSessionStampIfPresent(); + + evcs.getSessionEnd().setChargeSessionStampIfNotPresent(Instant.now(this.parent.componentManager.getClock()), + evcs.getActiveConsumptionEnergy().orElse(0L)); break; case Preparing: evcsStatus = Status.READY_FOR_CHARGING; @@ -259,6 +313,11 @@ public StatusNotificationConfirmation handleStatusNotificationRequest(UUID sessi evcsStatus = Status.ERROR; break; } + + if (ocppStatus != ChargePointStatus.Unavailable) { + evcs._setChargingstationCommunicationFailed(false); + } + if (evcsStatus != null) { evcs._setStatus(evcsStatus); } diff --git a/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/OcppServerImpl.java b/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/OcppServerImpl.java index 84a99134cc8..594c9419fc1 100644 --- a/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/OcppServerImpl.java +++ b/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/OcppServerImpl.java @@ -1,7 +1,7 @@ package io.openems.edge.evcs.ocpp.server; import java.net.UnknownHostException; -import java.util.Arrays; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,7 +50,7 @@ public class OcppServerImpl extends AbstractOpenemsComponent implements OpenemsC public static final int DEFAULT_PORT = 8887; private final Logger log = LoggerFactory.getLogger(OcppServerImpl.class); protected Config config; - + /** * The JSON server. * @@ -58,7 +58,7 @@ public class OcppServerImpl extends AbstractOpenemsComponent implements OpenemsC * Responsible for the OCPP communication. */ private final MyJsonServer myJsonServer = new MyJsonServer(this); - + /** * Currently connected sessions with their related evcs components. */ @@ -85,14 +85,16 @@ public class OcppServerImpl extends AbstractOpenemsComponent implements OpenemsC */ @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MULTIPLE) protected void addEvcs(Evcs evcs) { - if (!(evcs instanceof AbstractOcppEvcsComponent)) { + if (!(evcs instanceof AbstractOcppEvcsComponent) || evcs == null) { return; } AbstractOcppEvcsComponent ocppEvcs = (AbstractOcppEvcsComponent) evcs; - List presentEvcss = this.ocppEvcss.get(ocppEvcs.getConfiguredOcppId()); + List presentEvcss = ocppEvcss.get(ocppEvcs.getConfiguredOcppId()); + if (presentEvcss == null) { - this.ocppEvcss.put(ocppEvcs.getConfiguredOcppId(), Arrays.asList(ocppEvcs)); - presentEvcss = Arrays.asList(ocppEvcs); + List initEvcssArr = new ArrayList(); + initEvcssArr.add(ocppEvcs); + ocppEvcss.put(ocppEvcs.getConfiguredOcppId(), initEvcssArr); } else { presentEvcss.add(ocppEvcs); } @@ -108,15 +110,14 @@ protected void addEvcs(Evcs evcs) { /** * Removes the given Evcs component from the list and checks whether there is a - * present session that should be removed. + * present session that should be removed. * * @param evcs Evcs that should be removed */ protected void removeEvcs(Evcs evcs) { - if (!(evcs instanceof AbstractOcppEvcsComponent)) { + if (!(evcs instanceof AbstractOcppEvcsComponent) || evcs == null) { return; } - AbstractOcppEvcsComponent ocppEvcs = (AbstractOcppEvcsComponent) evcs; List evcss = this.activeEvcsSessions.get(ocppEvcs.getSessionId()); if (evcss != null) { @@ -129,7 +130,7 @@ protected void removeEvcs(Evcs evcs) { this.ocppEvcss.remove(ocppEvcs.getConfiguredOcppId()); ocppEvcs.lostSession(); } - + public OcppServerImpl() { super(OpenemsComponent.ChannelId.values() // ); diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/Config.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/Config.java index 9e3a52b971d..335ab77d36d 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/Config.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/Config.java @@ -17,12 +17,6 @@ @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") boolean enabled() default true; - @AttributeDefinition(name = "Datasource-ID", description = "ID of Simulator Datasource.") - String datasource_id(); - - @AttributeDefinition(name = "Datasource target filter", description = "This is auto-generated by 'Datasource-ID'.") - String datasource_target() default ""; - String webconsole_configurationFactory_nameHint() default "Simulator EVCS [{id}]"; } \ No newline at end of file diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatedEvcs.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatedEvcs.java index 147f548f362..dad6ffbc501 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatedEvcs.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/evcs/SimulatedEvcs.java @@ -12,25 +12,22 @@ 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.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; -import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.event.Event; import org.osgi.service.event.EventConstants; import org.osgi.service.event.EventHandler; import org.osgi.service.metatype.annotations.Designate; import io.openems.common.channel.Unit; -import io.openems.common.types.ChannelAddress; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.evcs.api.Evcs; +import io.openems.edge.evcs.api.EvcsPower; import io.openems.edge.evcs.api.ManagedEvcs; import io.openems.edge.evcs.api.Status; -import io.openems.edge.simulator.datasource.api.SimulatorDatasource; @Designate(ocd = Config.class, factory = true) @Component(name = "Simulator.Evcs", // @@ -39,6 +36,9 @@ public class SimulatedEvcs extends AbstractOpenemsComponent implements ManagedEvcs, Evcs, OpenemsComponent, EventHandler { + @Reference + private EvcsPower evcsPower; + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { SIMULATED_CHARGE_POWER(Doc.of(OpenemsType.INTEGER).unit(Unit.WATT)); @@ -59,26 +59,16 @@ public SimulatedEvcs() { ManagedEvcs.ChannelId.values(), // Evcs.ChannelId.values(), // ChannelId.values() // - ); } @Reference protected ConfigurationAdmin cm; - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) - protected SimulatorDatasource datasource; - @Activate void activate(ComponentContext context, Config config) throws IOException { super.activate(context, config.id(), config.alias(), config.enabled()); - - // update filter for 'datasource' - if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "datasource", config.datasource_id())) { - return; - } - - this._setMaximumHardwarePower(22800); + this._setMaximumHardwarePower(22080); this._setMinimumHardwarePower(6000); this._setPhases(3); this._setStatus(Status.CHARGING); @@ -106,31 +96,29 @@ public void handleEvent(Event event) { private double exactEnergySession = 0; private void updateChannels() { - int chargePower = 0; + int chargePowerLimit = this.getChargePower().orElse(0); Optional chargePowerLimitOpt = this.getSetChargePowerLimitChannel().getNextWriteValueAndReset(); if (chargePowerLimitOpt.isPresent()) { - int chargePowerLimit = chargePowerLimitOpt.get(); - - // copy write value to read value - this._setSetChargePowerLimit(chargePowerLimit); - - // get and store Simulated Charge Power - Integer simulatedChargePower = this.datasource.getValue(OpenemsType.INTEGER, - new ChannelAddress(this.id(), "ActivePower")); - this.channel(ChannelId.SIMULATED_CHARGE_POWER).setNextValue(simulatedChargePower); - - // Apply Charge Limit - if (simulatedChargePower != null) { - chargePower = Math.min(simulatedChargePower, chargePowerLimit); + chargePowerLimit = chargePowerLimitOpt.get(); + } + try { + if (chargePowerLimit > this.getChargePower().orElse(0)) { + this.setChargePowerLimitWithPid(chargePowerLimit); + } else { + this.setChargePowerLimit(chargePowerLimit); } + } catch (OpenemsNamedException e) { + e.printStackTrace(); } - - this._setChargePower(chargePower); + int sentPower = this.getSetChargePowerLimitChannel().getNextWriteValue().orElse(0); + this._setSetChargePowerLimit(sentPower); + this._setChargePower(sentPower); long timeDiff = ChronoUnit.MILLIS.between(this.lastUpdate, LocalDateTime.now()); - double energieTransfered = (timeDiff / 1000.0 / 60 / 60) * this.getChargePower().orElse(0); - this.exactEnergySession = this.exactEnergySession + energieTransfered; + double energyTransfered = (timeDiff / 1000.0 / 60.0 / 60.0) * this.getChargePower().orElse(0); + + this.exactEnergySession = this.exactEnergySession + energyTransfered; this._setEnergySession((int) this.exactEnergySession); this.lastUpdate = LocalDateTime.now(); @@ -141,4 +129,8 @@ public String debugLog() { return this.getChargePower().asString(); } + @Override + public EvcsPower getEvcsPower() { + return this.evcsPower; + } }