diff --git a/cnf/pom.xml b/cnf/pom.xml index 498503b5fdb..91fccb47644 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -207,7 +207,7 @@ org.ops4j.pax.logging pax-logging-api - 2.0.5 + 2.0.6 org.ops4j.pax.logging diff --git a/io.openems.common/src/io/openems/common/utils/JsonUtils.java b/io.openems.common/src/io/openems/common/utils/JsonUtils.java index ae25c5b1944..4dc4ada8973 100644 --- a/io.openems.common/src/io/openems/common/utils/JsonUtils.java +++ b/io.openems.common/src/io/openems/common/utils/JsonUtils.java @@ -27,8 +27,8 @@ import io.openems.common.exceptions.NotImplementedException; import io.openems.common.exceptions.OpenemsError; -import io.openems.common.exceptions.OpenemsException; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; public class JsonUtils { @@ -124,7 +124,7 @@ public static float getAsFloat(JsonElement jElement) throws OpenemsNamedExceptio } throw OpenemsError.JSON_NO_FLOAT.exception(jPrimitive.toString().replaceAll("%", "%%")); } - + public static float getAsFloat(JsonElement jElement, String memberName) throws OpenemsNamedException { JsonPrimitive jPrimitive = getAsPrimitive(jElement, memberName); if (jPrimitive.isNumber()) { @@ -725,6 +725,11 @@ public JsonObjectBuilder addProperty(String property, boolean value) { return this; } + public JsonObjectBuilder addProperty(String property, double value) { + j.addProperty(property, value); + return this; + } + public JsonObjectBuilder addPropertyIfNotNull(String property, String value) { if (value != null) { j.addProperty(property, value); @@ -753,6 +758,13 @@ public JsonObjectBuilder addPropertyIfNotNull(String property, Boolean value) { return this; } + public JsonObjectBuilder addPropertyIfNotNull(String property, Double value) { + if (value != null) { + j.addProperty(property, value); + } + return this; + } + public JsonObjectBuilder add(String property, JsonElement value) { j.add(property, value); return this; diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index fde7fb8b4f2..f81379b20c9 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -53,6 +53,7 @@ bnd.identity;id='io.openems.edge.controller.debug.detailedlog',\ bnd.identity;id='io.openems.edge.controller.debug.log',\ bnd.identity;id='io.openems.edge.controller.ess.acisland',\ + bnd.identity;id='io.openems.edge.controller.ess.activepowervoltagecharacteristic',\ bnd.identity;id='io.openems.edge.controller.ess.delaycharge',\ bnd.identity;id='io.openems.edge.controller.ess.delayedselltogrid',\ bnd.identity;id='io.openems.edge.controller.ess.hybrid.surplusfeedtogrid',\ @@ -60,6 +61,7 @@ bnd.identity;id='io.openems.edge.controller.ess.mindischargeperiod',\ bnd.identity;id='io.openems.edge.controller.ess.onefullcycle',\ bnd.identity;id='io.openems.edge.controller.ess.predictivedelaycharge',\ + bnd.identity;id='io.openems.edge.controller.ess.reactivepowervoltagecharacteristic',\ bnd.identity;id='io.openems.edge.controller.ess.selltogridlimit',\ bnd.identity;id='io.openems.edge.controller.evcs',\ bnd.identity;id='io.openems.edge.controller.evcs.fixactivepower',\ @@ -79,7 +81,6 @@ bnd.identity;id='io.openems.edge.controller.symmetric.linearpowerband',\ bnd.identity;id='io.openems.edge.controller.symmetric.peakshaving',\ bnd.identity;id='io.openems.edge.controller.symmetric.randompower',\ - bnd.identity;id='io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic',\ bnd.identity;id='io.openems.edge.controller.symmetric.timeslotpeakshaving',\ bnd.identity;id='io.openems.edge.core',\ bnd.identity;id='io.openems.edge.ess.byd.container',\ @@ -176,6 +177,7 @@ io.openems.edge.controller.debug.detailedlog;version=snapshot,\ io.openems.edge.controller.debug.log;version=snapshot,\ io.openems.edge.controller.ess.acisland;version=snapshot,\ + io.openems.edge.controller.ess.activepowervoltagecharacteristic;version=snapshot,\ io.openems.edge.controller.ess.delaycharge;version=snapshot,\ io.openems.edge.controller.ess.delayedselltogrid;version=snapshot,\ io.openems.edge.controller.ess.hybrid.surplusfeedtogrid;version=snapshot,\ @@ -183,6 +185,7 @@ io.openems.edge.controller.ess.mindischargeperiod;version=snapshot,\ io.openems.edge.controller.ess.onefullcycle;version=snapshot,\ io.openems.edge.controller.ess.predictivedelaycharge;version=snapshot,\ + io.openems.edge.controller.ess.reactivepowervoltagecharacteristic;version=snapshot,\ io.openems.edge.controller.ess.selltogridlimit;version=snapshot,\ io.openems.edge.controller.evcs;version=snapshot,\ io.openems.edge.controller.evcs.fixactivepower;version=snapshot,\ @@ -202,7 +205,6 @@ io.openems.edge.controller.symmetric.linearpowerband;version=snapshot,\ io.openems.edge.controller.symmetric.peakshaving;version=snapshot,\ io.openems.edge.controller.symmetric.randompower;version=snapshot,\ - io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic;version=snapshot,\ io.openems.edge.controller.symmetric.timeslotpeakshaving;version=snapshot,\ io.openems.edge.core;version=snapshot,\ io.openems.edge.ess.api;version=snapshot,\ diff --git a/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/PolyLine.java b/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/PolyLine.java new file mode 100644 index 00000000000..b7d40562ea6 --- /dev/null +++ b/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/PolyLine.java @@ -0,0 +1,110 @@ +package io.openems.edge.common.linecharacteristic; + +import java.util.Map.Entry; +import java.util.TreeMap; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.utils.JsonUtils; + +/** + * Defines a polyline built of multiple points defined by a JsonArray. + * + *

+ * This class can be used e.g. to build Q-by-U characteristics Controllers. + */ +public class PolyLine { + + private final TreeMap points; + + /** + * Creates a PolyLine from two points. + * + * @param x1 'x' value of point 1 + * @param y1 'y' value of point 1 + * @param x2 'x' value of point 2 + * @param y2 'y' value of point 2 + * @throws OpenemsNamedException on error + */ + public PolyLine(Float x1, Float y1, Float x2, Float y2) throws OpenemsNamedException { + TreeMap points = new TreeMap<>(); + points.put(x1, y1); + points.put(x2, y2); + this.points = points; + } + + /** + * Creates a PolyLine from a JSON line configuration. + * + * @param x the name of the 'x' value inside the Json-Array + * @param y the name of the 'y' value inside the Json-Array + * @param lineConfig the configured x and y coordinates values; parsed to a + * Json-Array + * @throws OpenemsNamedException on error + */ + public PolyLine(String x, String y, String lineConfig) throws OpenemsNamedException { + this(x, y, JsonUtils.getAsJsonArray(JsonUtils.parse(lineConfig))); + } + + /** + * Creates a PolyLine from a JSON line configuration. + * + *

+ * Parse the given JSON line format to x and y parameters. + * + *

+	 * [
+	 *  { "x": 0.9,  "y":-4000 },
+	 *  { "x": 0.93, "y":-1000 },
+	 *  { "x": 1.07, "y":1000 },
+	 *  { "x": 1.1,  "y":4000 }
+	 * ]
+	 * 
+ * + * @param x the name of the 'x' value inside the Json-Array + * @param y the name of the 'y' value inside the Json-Array + * @param lineConfig the configured x and y coordinates values + * @throws OpenemsNamedException on error + */ + public PolyLine(String x, String y, JsonArray lineConfig) throws OpenemsNamedException { + TreeMap points = new TreeMap<>(); + for (JsonElement element : lineConfig) { + Float xValue = JsonUtils.getAsFloat(element, x); + Float yValue = JsonUtils.getAsFloat(element, y); + points.put(xValue, yValue); + } + this.points = points; + } + + /** + * Gets the Y-value for the given X. + * + * @param x the 'x' value + * @return the 'y' value + * @throws OpenemsNamedException on error + */ + public Float getValue(float x) throws OpenemsNamedException { + Entry floorEntry = this.points.floorEntry(x); + Entry ceilingEntry = this.points.ceilingEntry(x); + + if (floorEntry == null && ceilingEntry == null) { + return null; + + } else if (floorEntry == null) { + return ceilingEntry.getValue(); + + } else if (ceilingEntry == null) { + return floorEntry.getValue(); + + } else if (floorEntry.equals(ceilingEntry)) { + return floorEntry.getValue(); + + } else { + Float m = (ceilingEntry.getValue() - floorEntry.getValue()) / (ceilingEntry.getKey() - floorEntry.getKey()); + Float t = floorEntry.getValue() - m * floorEntry.getKey(); + return m * x + t; + } + } +} diff --git a/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/package-info.java b/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/package-info.java new file mode 100644 index 00000000000..8e3a5b2e198 --- /dev/null +++ b/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.common.linecharacteristic; diff --git a/io.openems.edge.common/test/io/openems/edge/common/linecharacteristic/PolyLineTest.java b/io.openems.edge.common/test/io/openems/edge/common/linecharacteristic/PolyLineTest.java new file mode 100644 index 00000000000..188806d300e --- /dev/null +++ b/io.openems.edge.common/test/io/openems/edge/common/linecharacteristic/PolyLineTest.java @@ -0,0 +1,63 @@ +package io.openems.edge.common.linecharacteristic; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.google.gson.JsonArray; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.utils.JsonUtils; + +public class PolyLineTest { + + @Test + public void test() throws OpenemsNamedException { + JsonArray lineConfig = JsonUtils.buildJsonArray()// + .add(JsonUtils.buildJsonObject()// + .addProperty("xCoord", 0.9) // + .addProperty("yCoord", 60) // + .build()) // + .add(JsonUtils.buildJsonObject()// + .addProperty("xCoord", 0.93) // + .addProperty("yCoord", 0) // + .build()) // + .add(JsonUtils.buildJsonObject()// + .addProperty("xCoord", 1.07) // + .addProperty("yCoord", 0) // + .build()) // + .add(JsonUtils.buildJsonObject()// + .addProperty("xCoord", 1.1) // + .addProperty("yCoord", -60) // + .build() // + ).build(); + + PolyLine polyline = new PolyLine("xCoord", "yCoord", lineConfig); + + // exactly first + assertEquals(60f, polyline.getValue(0.9f), 0.00001); + + // exactly last + assertEquals(-60f, polyline.getValue(1.1f), 0.00001); + + // beyond last + assertEquals(-60f, polyline.getValue(1.2f), 0.00001); + + // before first + assertEquals(60f, polyline.getValue(0.7f), 0.00001); + + // between first two + assertEquals(30f, polyline.getValue(0.915f), 0.001); + } + + @Test + public void testEmpty() throws OpenemsNamedException { + JsonArray lineConfig = JsonUtils.buildJsonArray().build(); + + PolyLine polyline = new PolyLine("xCoord", "yCoord", lineConfig); + + // exactly first + assertEquals(null, polyline.getValue(0.9f)); + } + +} diff --git a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/.classpath b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/.classpath similarity index 100% rename from io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/.classpath rename to io.openems.edge.controller.ess.activepowervoltagecharacteristic/.classpath diff --git a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/.gitignore b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/.gitignore similarity index 100% rename from io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/.gitignore rename to io.openems.edge.controller.ess.activepowervoltagecharacteristic/.gitignore diff --git a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/.project b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/.project similarity index 85% rename from io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/.project rename to io.openems.edge.controller.ess.activepowervoltagecharacteristic/.project index 4cdc4585566..ad8d001b963 100644 --- a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/.project +++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/.project @@ -1,6 +1,6 @@ - io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic + io.openems.edge.controller.ess.activepowervoltagecharacteristic diff --git a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/bnd.bnd b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/bnd.bnd similarity index 78% rename from io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/bnd.bnd rename to io.openems.edge.controller.ess.activepowervoltagecharacteristic/bnd.bnd index c04d25c6eec..c21deeed797 100644 --- a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/bnd.bnd +++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/bnd.bnd @@ -1,4 +1,4 @@ -Bundle-Name: OpenEMS Edge Controller Symmetric Reactive-Power Voltage Characteristics +Bundle-Name: OpenEMS Edge Controller Ess Active Power Voltage Characteristics Bundle-Vendor: FENECON GmbH Bundle-License: https://opensource.org/licenses/EPL-2.0 Bundle-Version: 1.0.0.${tstamp} diff --git a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/readme.adoc b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/readme.adoc new file mode 100644 index 00000000000..345b8c13927 --- /dev/null +++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/readme.adoc @@ -0,0 +1,40 @@ += Ess Active Power Voltage Characteristic + +* This controller charges/discharges an energy storage system with active power in order to keep the grid voltage within an acceptable range. + +* The active power set-point depends on the ratio of the grid voltage divided by the configured `nominalVoltage` parameter. + +* The active power per voltage ratio is defined by a poly-line graph in the `lineConfig` parameter. It takes a Json-Array of the following form: + +---- +[ + { + "voltageRatio":0.95, + "power":4000 + }, + { + "voltageRatio":0.97999, + "power":1000 + }, + { + "voltageRatio":0.98, + "power":0 + }, + { + "voltageRatio":1.0299, + "power":0 + }, + { + "voltageRatio":1.03, + "power":-1000 + }, + { + "voltageRatio":1.05, + "power":-4000 + } +] +---- + + * `waitForHysteresis` parameter: active power set-point is not updated more often than the hysteresis time in seconds. Default value is 20 seconds. The purpose of this parameter is to avoid oscillation in grid voltage via frequently setting power. + +https://github.com/OpenEMS/openems/tree/feature/develop/io.openems.edge.controller.ess.activepowervoltagecharacteristic[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristic.java b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristic.java new file mode 100644 index 00000000000..caadf870e7e --- /dev/null +++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristic.java @@ -0,0 +1,69 @@ +package io.openems.edge.controller.ess.activepowervoltagecharacteristic; + +import io.openems.common.channel.Unit; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.FloatReadChannel; +import io.openems.edge.common.channel.IntegerReadChannel; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.controller.api.Controller; + +public interface ActivePowerVoltageCharacteristic extends Controller, OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + CALCULATED_POWER(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT)), // + VOLTAGE_RATIO(Doc.of(OpenemsType.FLOAT)), // + ; + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + /** + * Gets the Channel for {@link ChannelId#CALCULATED_POWER}. + * + * @return the Channel + */ + public default IntegerReadChannel getCalculatedPowerChannel() { + return this.channel(ChannelId.CALCULATED_POWER); + } + + /** + * Internal method to set the 'nextValue' on {@link ChannelId#CALCULATED_POWER} + * Channel. + * + * @param value the next value + */ + public default void _setCalculatedPower(Integer value) { + this.getCalculatedPowerChannel().setNextValue(value); + } + + /** + * Gets the Channel for {@link ChannelId#VOLTAGE_RATIO}. + * + * @return the Channel + */ + public default FloatReadChannel getVoltageRatioChannel() { + return this.channel(ChannelId.VOLTAGE_RATIO); + } + + /** + * Internal method to set the 'nextValue' on {@link ChannelId#VOLTAGE_RATIO} + * Channel. + * + * @param value the next value + */ + public default void _setVoltageRatio(Float value) { + this.getVoltageRatioChannel().setNextValue(value); + } + +} diff --git a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristicImpl.java b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristicImpl.java new file mode 100644 index 00000000000..2f514c01d8e --- /dev/null +++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristicImpl.java @@ -0,0 +1,140 @@ +package io.openems.edge.controller.ess.activepowervoltagecharacteristic; + +import java.time.Clock; +import java.time.Duration; +import java.time.LocalDateTime; + +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.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.channel.value.Value; +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.linecharacteristic.PolyLine; +import io.openems.edge.common.sum.GridMode; +import io.openems.edge.controller.api.Controller; +import io.openems.edge.ess.api.ManagedSymmetricEss; +import io.openems.edge.meter.api.SymmetricMeter; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Controller.Ess.ActivePowerVoltageCharacteristic", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE// +) +public class ActivePowerVoltageCharacteristicImpl extends AbstractOpenemsComponent + implements ActivePowerVoltageCharacteristic, Controller, OpenemsComponent { + + private final Logger log = LoggerFactory.getLogger(ActivePowerVoltageCharacteristicImpl.class); + + private LocalDateTime lastSetPowerTime = LocalDateTime.MIN; + + private Config config; + private PolyLine pByUCharacteristics = null; + + @Reference + protected ConfigurationAdmin cm; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + private SymmetricMeter meter; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) + private ManagedSymmetricEss ess; + + @Reference + protected ComponentManager componentManager; + + public ActivePowerVoltageCharacteristicImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values(), // + ActivePowerVoltageCharacteristic.ChannelId.values()// + ); + } + + @Activate + void activate(ComponentContext context, Config config) throws OpenemsNamedException { + super.activate(context, config.id(), config.alias(), config.enabled()); + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "ess", config.ess_id())) { + return; + } + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "meter", config.meter_id())) { + return; + } + this.config = config; + this.pByUCharacteristics = new PolyLine("voltageRatio", "power", config.lineConfig()); + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + public void run() throws OpenemsNamedException { + GridMode gridMode = this.ess.getGridMode(); + if (gridMode.isUndefined()) { + this.logWarn(this.log, "Grid-Mode is [UNDEFINED]"); + } + switch (gridMode) { + case ON_GRID: + case UNDEFINED: + break; + case OFF_GRID: + return; + default: + break; + } + + // Ratio between current voltage and nominal voltage + final Float voltageRatio; + Value gridVoltage = this.meter.getVoltage(); + if (gridVoltage.isDefined()) { + voltageRatio = gridVoltage.get() / (this.config.nominalVoltage() * 1000); + } else { + voltageRatio = null; + } + this._setVoltageRatio(voltageRatio); + if (voltageRatio == null) { + return; + } + + // Do NOT change Set Power If it Does not exceed the hysteresis time + Clock clock = this.componentManager.getClock(); + LocalDateTime now = LocalDateTime.now(clock); + if (Duration.between(this.lastSetPowerTime, now).getSeconds() < this.config.waitForHysteresis()) { + return; + } + this.lastSetPowerTime = now; + + // Get P-by-U value from voltageRatio + final Integer power; + if (this.pByUCharacteristics == null) { + power = null; + } else { + Float p = this.pByUCharacteristics.getValue(voltageRatio); + if (p == null) { + power = null; + } else { + power = p.intValue(); + } + } + this._setCalculatedPower(power); + + // Apply Power + this.ess.setActivePowerEquals(power); + } +} diff --git a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/Config.java b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/Config.java new file mode 100644 index 00000000000..158272853a6 --- /dev/null +++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/Config.java @@ -0,0 +1,42 @@ +package io.openems.edge.controller.ess.activepowervoltagecharacteristic; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(// + name = "Controller Ess Voltage Active Power Characteristic", // + description = "Defines a active power voltage characteristic for an energy storage system.") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "ctrlActiveCharacteristic0"; + + @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") + String alias() default ""; + + @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") + boolean enabled() default true; + + @AttributeDefinition(name = "Ess-ID", description = "ID of Ess device.") + String ess_id(); + + @AttributeDefinition(name = "Meter-ID", description = "ID of Meter.") + String meter_id(); + + @AttributeDefinition(name = "P by U characteristic ", description = "The graph values for voltage-ratio and power") + String lineConfig() default "[{ \"voltageRatio\" : 0.95, \"power\" : 4000}, { \"voltageRatio\":0.97999, \"power\": 1000},{ \"voltageRatio\" : 0.98, \"power\" : 0},{ \"voltageRatio\" : 1.0299, \"power\" : 0},{\"voltageRatio\" :1.03 , \"power\": -1000 },{\"voltageRatio\": 1.05 , \"power\": -4000 }]"; + + @AttributeDefinition(name = "Nominal Voltage [V]", description = "The nominal voltage of the grid") + float nominalVoltage() default 240f; + + @AttributeDefinition(name = "Hysteresis [second]", description = "Wait For Hysteresis to Change the Set Power") + int waitForHysteresis() default 20; + + @AttributeDefinition(name = "Ess target filter", description = "This is auto-generated by 'Ess-ID'.") + String ess_target() default ""; + + @AttributeDefinition(name = "Meter target filter", description = "This is auto-generated by 'Meter-ID'.") + String meter_target() default ""; + + String webconsole_configurationFactory_nameHint() default "Controller Ess Voltage Active Power Characteristic [{id}]"; +} \ No newline at end of file diff --git a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/test/.gitignore b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/.gitignore similarity index 100% rename from io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/test/.gitignore rename to io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/.gitignore diff --git a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java new file mode 100644 index 00000000000..253f2c96d8a --- /dev/null +++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java @@ -0,0 +1,123 @@ +package io.openems.edge.controller.ess.activepowervoltagecharacteristic; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; + +import org.junit.Test; + +import io.openems.common.types.ChannelAddress; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.common.test.TimeLeapClock; +import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.meter.test.DummyAsymmetricMeter; + +public class CharacteristicImplTest { + + private static final String CTRL_ID = "ctrlActivePowerVoltageCharacteristic0"; + private static final String ESS_ID = "ess1"; + private static final String METER_ID = "meter0"; + private static final ChannelAddress ESS_ACTIVE_POWER = new ChannelAddress(ESS_ID, "SetActivePowerEquals"); + private static final ChannelAddress METER_VOLTAGE = new ChannelAddress(METER_ID, "Voltage"); + + @Test + public void test() throws Exception { + final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-10-05T14:00:00.00Z"), ZoneOffset.UTC); + new ControllerTest(new ActivePowerVoltageCharacteristicImpl())// + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("meter", new DummyAsymmetricMeter(METER_ID)) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .activate(MyConfig.create()// + .setId(CTRL_ID)// + .setEssId(ESS_ID)// + .setMeterId(METER_ID)// + .setNominalVoltage(240)// + .setWaitForHysteresis(5)// + .setPowerVoltConfig(JsonUtils.buildJsonArray()// + .add(JsonUtils.buildJsonObject()// + .addProperty("voltageRatio", 0.95) // + .addProperty("power", 4000) // + .build()) // + .add(JsonUtils.buildJsonObject()// + .addProperty("voltageRatio", 0.98) // + .addProperty("power", 1000) // + .build()) // + .add(JsonUtils.buildJsonObject()// + .addProperty("voltageRatio", 0.98001) // + .addProperty("power", 0) // + .build()) // + .add(JsonUtils.buildJsonObject()// + .addProperty("voltageRatio", 1.02999) // + .addProperty("power", 0) // + .build()) // + .add(JsonUtils.buildJsonObject()// + .addProperty("voltageRatio", 1.03) // + .addProperty("power", -1000) // + .build()) // + .add(JsonUtils.buildJsonObject()// + .addProperty("voltageRatio", 1.05) // + .addProperty("power", -4000) // + .build() // + ).build().toString() // + ).build()) // + .next(new TestCase("First Input") // + .input(METER_VOLTAGE, 250_000)) // [mV] + .next(new TestCase("Power: -2750 first") // + .output(ESS_ACTIVE_POWER, -2750))// + .next(new TestCase() // + .output(ESS_ACTIVE_POWER, -2750))// + .next(new TestCase("Second Input, \"Power: -1500 \"") // + .timeleap(clock, 5, ChronoUnit.SECONDS) // + .input(METER_VOLTAGE, 248_000) // [mV] + .output(ESS_ACTIVE_POWER, -1500))// + .next(new TestCase() // + .output(ESS_ACTIVE_POWER, -1500))// + .next(new TestCase() // + .input(METER_VOLTAGE, 240_200) // [mV] + .output(ESS_ACTIVE_POWER, -1500))// + .next(new TestCase("Third Input, \"Power: 0 \"") // + .timeleap(clock, 5, ChronoUnit.SECONDS) // + .input(METER_VOLTAGE, 238_100)// [mV] + .output(ESS_ACTIVE_POWER, 0))// + .next(new TestCase() // + .input(METER_VOLTAGE, 240_000)// [mV] + .output(ESS_ACTIVE_POWER, 0))// + .next(new TestCase() // + .input(METER_VOLTAGE, 238_800)// [mV] + .output(ESS_ACTIVE_POWER, 0))// + .next(new TestCase("Fourth Input, \"Power: 0 \"") // + .timeleap(clock, 5, ChronoUnit.SECONDS) // + .input(METER_VOLTAGE, 235_200)// [mV] + .output(ESS_ACTIVE_POWER, 1000))// + .next(new TestCase() // + .timeleap(clock, 2, ChronoUnit.SECONDS) // + .input(METER_VOLTAGE, 235_600)// [mV] + .output(ESS_ACTIVE_POWER, 1000))// + .next(new TestCase() // + .timeleap(clock, 2, ChronoUnit.SECONDS) // + .input(METER_VOLTAGE, 234_000)// [mV] + .output(ESS_ACTIVE_POWER, 1000))// + .next(new TestCase("Fifth Input, \"Power: 1625 \"") // + .timeleap(clock, 1, ChronoUnit.SECONDS) // + .input(METER_VOLTAGE, 233_700)// [mV] + .output(ESS_ACTIVE_POWER, 1625))// + .next(new TestCase("Fourth Input, \"Power: 0 \"") // + .timeleap(clock, 5, ChronoUnit.SECONDS) // + .input(METER_VOLTAGE, 225_000)// [mV] + .output(ESS_ACTIVE_POWER, 4000))// + .next(new TestCase("Smaller then Min Key, \"Power: 0 \"") // + .timeleap(clock, 5, ChronoUnit.SECONDS) // + .input(METER_VOLTAGE, 255_000)// [mV] + .output(ESS_ACTIVE_POWER, -4000))// + .next(new TestCase("Bigger than Max Key, \"Power: 0 \"") // + .timeleap(clock, 5, ChronoUnit.SECONDS) // + .input(METER_VOLTAGE, 270_000)// [mV] + .output(ESS_ACTIVE_POWER, -4000))// + ; + } +} diff --git a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/MyConfig.java b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/MyConfig.java new file mode 100644 index 00000000000..afc868684f9 --- /dev/null +++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/MyConfig.java @@ -0,0 +1,108 @@ +package io.openems.edge.controller.ess.activepowervoltagecharacteristic; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.utils.ConfigUtils; +import io.openems.edge.common.test.AbstractComponentConfig; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private String essId; + private String meterId; + private int nominalVoltage; + private int waitForHysteresis; + private String powerVoltConfig; + + private Builder() { + + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setEssId(String essId) { + this.essId = essId; + return this; + } + + public Builder setMeterId(String meterId) { + this.meterId = meterId; + return this; + } + + public Builder setNominalVoltage(int nominalVoltage) { + this.nominalVoltage = nominalVoltage; + return this; + } + + public Builder setPowerVoltConfig(String powerVoltConfig) throws OpenemsNamedException { + this.powerVoltConfig = powerVoltConfig; + return this; + } + + public Builder setWaitForHysteresis(int waitForHysteresis) throws OpenemsNamedException { + this.waitForHysteresis = waitForHysteresis; + return this; + } + + public MyConfig build() { + return new MyConfig(this); + } + + } + + /** + * Create a Config builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + private final Builder builder; + + private MyConfig(Builder builder) { + super(Config.class, builder.id); + this.builder = builder; + } + + @Override + public String ess_id() { + return this.builder.essId; + } + + @Override + public String meter_id() { + return this.builder.meterId; + } + + @Override + public String lineConfig() { + return this.builder.powerVoltConfig; + } + + @Override + public float nominalVoltage() { + return this.builder.nominalVoltage; + } + + @Override + public int waitForHysteresis() { + return this.builder.waitForHysteresis; + } + + @Override + public String ess_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.ess_id()); + } + + @Override + public String meter_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.meter_id()); + } +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/.classpath b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/.gitignore b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/.gitignore new file mode 100644 index 00000000000..c2b941a96de --- /dev/null +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/.gitignore @@ -0,0 +1,2 @@ +/bin_test/ +/generated/ diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/.project b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/.project new file mode 100644 index 00000000000..769d7b8da81 --- /dev/null +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/.project @@ -0,0 +1,23 @@ + + + io.openems.edge.controller.ess.reactivepowervoltagecharacteristic + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/bnd.bnd b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/bnd.bnd new file mode 100644 index 00000000000..e07562d713f --- /dev/null +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/bnd.bnd @@ -0,0 +1,15 @@ +Bundle-Name: OpenEMS Edge Controller Ess Reactive Power Voltage Characteristics +Bundle-Vendor: FENECON GmbH +Bundle-License: https://opensource.org/licenses/EPL-2.0 +Bundle-Version: 1.0.0.${tstamp} + +-buildpath: \ + ${buildpath},\ + io.openems.common,\ + io.openems.edge.common,\ + io.openems.edge.controller.api,\ + io.openems.edge.ess.api,\ + io.openems.edge.meter.api + +-testpath: \ + ${testpath} diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/readme.adoc b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/readme.adoc new file mode 100644 index 00000000000..a507bfce18e --- /dev/null +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/readme.adoc @@ -0,0 +1,34 @@ += Ess Reactive Power Voltage Characteristic + +* This controller sets reactive power of an energy storage system in order to keep the grid voltage within an acceptable range. + +* The reactive power set-point depends on the ratio of the grid voltage divided by the configured `nominalVoltage` parameter. + +* The reactive power set-point is defined as percent of the maximum apparent power of the inverter. + +* The reactive power per voltage ratio is defined by a poly-line graph in the `lineConfig` parameter. It takes a Json-Array of the following form: + +---- +[ + { + "voltageRatio":0.9, + "percent":60 + }, + { + "voltageRatio":0.93, + "percent":0 + }, + { + "voltageRatio":1.07 + "percent":0 + }, + { + "voltageRatio":1.1, + "percent":-60 + } +] +---- + + * `waitForHysteresis` parameter: reactive power set-point is not updated more often than the hysteresis time in seconds. Default value is 20 seconds. The purpose of this parameter is to avoid oscillation in grid voltage via frequently setting power. + +https://github.com/OpenEMS/openems/tree/feature/develop/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/symmetric/reactivepowervoltagecharacteristic/Config.java b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/Config.java similarity index 65% rename from io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/symmetric/reactivepowervoltagecharacteristic/Config.java rename to io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/Config.java index d4b5410f308..f0d6fe260a4 100644 --- a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/symmetric/reactivepowervoltagecharacteristic/Config.java +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/Config.java @@ -1,15 +1,15 @@ -package io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic; +package io.openems.edge.controller.ess.reactivepowervoltagecharacteristic; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -@ObjectClassDefinition( // - name = "Controller Reactive-Power Voltage Characteristic Symmetric", // - description = "Defines a reactive power voltage characteristic for storage system.") +@ObjectClassDefinition(// + name = "Controller Ess Voltage Reactive Power Characteristic", // + description = "Defines a reactive power voltage characteristic for an energy storage system.") @interface Config { @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") - String id() default "ctrlRctvPwrVltgChrctrstc0"; + String id() default "ctrlReactiveCharacteristic0"; @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") String alias() default ""; @@ -24,10 +24,13 @@ String meter_id(); @AttributeDefinition(name = "Q by U characteristic ", description = "The graph values for power and percentage") - String percentQ() default "[{ \"voltage\" : 0.9,\"percent\" : 60 }, { \"voltage\":0.93,\"percent\": 0},{\"voltage\":1.07 ,\"percent\": 0 },{\"voltage\": 1.1 ,\"percent\": -60 }]"; + String lineConfig() default "[{ \"voltageRatio\" : 0.9,\"percent\" : 60 }, { \"voltageRatio\":0.93,\"percent\": 0},{\"voltageRatio\":1.07 ,\"percent\": 0 },{\"voltageRatio\": 1.1 ,\"percent\": -60 }]"; @AttributeDefinition(name = "Nominal Voltage [V]", description = "The nominal voltage of the grid") - float nominalVoltage() default 230f; + float nominalVoltage() default 240f; + + @AttributeDefinition(name = "Hysteresis [second]", description = "Wait For Hysteresis to Change the Set Power") + int waitForHysteresis() default 20; @AttributeDefinition(name = "Ess target filter", description = "This is auto-generated by 'Ess-ID'.") String ess_target() default ""; @@ -35,5 +38,6 @@ @AttributeDefinition(name = "Meter target filter", description = "This is auto-generated by 'Meter-ID'.") String meter_target() default ""; - String webconsole_configurationFactory_nameHint() default "Controller Reactive-Power Voltage Characteristic Symmetric [{id}]"; + String webconsole_configurationFactory_nameHint() default "Controller Ess Voltage Reactive Power Characteristic [{id}]"; + } \ No newline at end of file diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePowerVoltageCharacteristic.java b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePowerVoltageCharacteristic.java new file mode 100644 index 00000000000..533bb3bfb51 --- /dev/null +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePowerVoltageCharacteristic.java @@ -0,0 +1,69 @@ +package io.openems.edge.controller.ess.reactivepowervoltagecharacteristic; + +import io.openems.common.channel.Unit; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.FloatReadChannel; +import io.openems.edge.common.channel.IntegerReadChannel; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.controller.api.Controller; + +public interface ReactivePowerVoltageCharacteristic extends Controller, OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + CALCULATED_POWER(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT)), // + VOLTAGE_RATIO(Doc.of(OpenemsType.FLOAT)), // + ; + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + /** + * Gets the Channel for {@link ChannelId#CALCULATED_POWER}. + * + * @return the Channel + */ + public default IntegerReadChannel getCalculatedPowerChannel() { + return this.channel(ChannelId.CALCULATED_POWER); + } + + /** + * Internal method to set the 'nextValue' on {@link ChannelId#CALCULATED_POWER} + * Channel. + * + * @param value the next value + */ + public default void _setCalculatedPower(Integer value) { + this.getCalculatedPowerChannel().setNextValue(value); + } + + /** + * Gets the Channel for {@link ChannelId#VOLTAGE_RATIO}. + * + * @return the Channel + */ + public default FloatReadChannel getVoltageRatioChannel() { + return this.channel(ChannelId.VOLTAGE_RATIO); + } + + /** + * Internal method to set the 'nextValue' on {@link ChannelId#VOLTAGE_RATIO} + * Channel. + * + * @param value the next value + */ + public default void _setVoltageRatio(Float value) { + this.getVoltageRatioChannel().setNextValue(value); + } + +} diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePwrVoltChractersticImpl.java b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePwrVoltChractersticImpl.java new file mode 100644 index 00000000000..2cf58d63a58 --- /dev/null +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePwrVoltChractersticImpl.java @@ -0,0 +1,147 @@ +package io.openems.edge.controller.ess.reactivepowervoltagecharacteristic; + +import java.time.Clock; +import java.time.Duration; +import java.time.LocalDateTime; + +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.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.channel.value.Value; +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.linecharacteristic.PolyLine; +import io.openems.edge.common.sum.GridMode; +import io.openems.edge.controller.api.Controller; +import io.openems.edge.ess.api.ManagedSymmetricEss; +import io.openems.edge.meter.api.SymmetricMeter; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Controller.Ess.ReactivePowerVoltageCharacteristic", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class ReactivePwrVoltChractersticImpl extends AbstractOpenemsComponent + implements ReactivePowerVoltageCharacteristic, Controller, OpenemsComponent { + + private final Logger log = LoggerFactory.getLogger(ReactivePwrVoltChractersticImpl.class); + + private LocalDateTime lastSetPowerTime = LocalDateTime.MIN; + + private Config config; + private PolyLine qByUCharacteristics = null; + + @Reference + protected ConfigurationAdmin cm; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + private SymmetricMeter meter; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) + private ManagedSymmetricEss ess; + + @Reference + protected ComponentManager componentManager; + + public ReactivePwrVoltChractersticImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values(), // + ReactivePowerVoltageCharacteristic.ChannelId.values()// + ); + } + + @Activate + void activate(ComponentContext context, Config config) throws OpenemsNamedException { + super.activate(context, config.id(), config.alias(), config.enabled()); + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "ess", config.ess_id())) { + return; + } + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "meter", config.meter_id())) { + return; + } + this.config = config; + this.qByUCharacteristics = new PolyLine("voltageRatio", "percent", config.lineConfig()); + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + public void run() throws OpenemsNamedException { + GridMode gridMode = this.ess.getGridMode(); + if (gridMode.isUndefined()) { + this.logWarn(this.log, "Grid-Mode is [UNDEFINED]"); + } + switch (gridMode) { + case ON_GRID: + case UNDEFINED: + break; + case OFF_GRID: + return; + default: + break; + } + + // Ratio between current voltage and nominal voltage + final Float voltageRatio; + Value gridVoltage = this.meter.getVoltage(); + if (gridVoltage.isDefined()) { + voltageRatio = gridVoltage.get() / (this.config.nominalVoltage() * 1000); + } else { + voltageRatio = null; + } + this._setVoltageRatio(voltageRatio); + if (voltageRatio == null) { + return; + } + + // Do NOT change Set Power If it Does not exceed the hysteresis time + Clock clock = this.componentManager.getClock(); + LocalDateTime now = LocalDateTime.now(clock); + if (Duration.between(this.lastSetPowerTime, now).getSeconds() < this.config.waitForHysteresis()) { + return; + } + this.lastSetPowerTime = now; + + // Get P-by-U value from voltageRatio + final Integer percent; + if (this.qByUCharacteristics == null) { + percent = null; + } else { + Float p = this.qByUCharacteristics.getValue(voltageRatio); + if (p == null) { + percent = null; + } else { + percent = p.intValue(); + } + } + + // Gets required maxApparentPower + // which is used in calculation of reactive power: + // Otherwise should not calcula the reactive power and has to return here + Value apparentPower = this.ess.getMaxApparentPower(); + Integer power = (int) (apparentPower.orElse(0) * percent * 0.01); + this._setCalculatedPower(power); + + // Apply Power + this.ess.setReactivePowerEquals(power); + } + +} diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/.gitignore b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/CharacteristicImplTest.java b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/CharacteristicImplTest.java new file mode 100644 index 00000000000..b47cf62db3d --- /dev/null +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/CharacteristicImplTest.java @@ -0,0 +1,103 @@ +package io.openems.edge.controller.ess.reactivepowervoltagecharacteristic; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; + +import org.junit.Test; + +import io.openems.common.types.ChannelAddress; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.common.test.TimeLeapClock; +import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.meter.test.DummySymmetricMeter; + +public class CharacteristicImplTest { + + private static final String CTRL_ID = "ctrlReactivePowerVoltageCharacteristic0"; + private static final String ESS_ID = "ess0"; + private static final String METER_ID = "meter0"; + private static final ChannelAddress ESS_REACTIVE_POWER = new ChannelAddress(ESS_ID, "SetReactivePowerEquals"); + private static final ChannelAddress METER_VOLTAGE = new ChannelAddress(METER_ID, "Voltage"); + private static final ChannelAddress MAX_APPARENT_POWER = new ChannelAddress(ESS_ID, "MaxApparentPower"); + + @Test + public void test() throws Exception { + final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-10-05T14:00:00.00Z"), ZoneOffset.UTC); + new ControllerTest(new ReactivePwrVoltChractersticImpl())// + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .addReference("meter", new DummySymmetricMeter(METER_ID)) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .activate(MyConfig.create()// + .setId(CTRL_ID)// + .setEssId(ESS_ID)// + .setMeterId(METER_ID)// + .setNominalVoltage(240)// + .setWaitForHysteresis(5)// + .setPowerVoltConfig(JsonUtils.buildJsonArray()// + .add(JsonUtils.buildJsonObject()// + .addProperty("voltageRatio", 0.9) // + .addProperty("percent", 60) // + .build()) // + .add(JsonUtils.buildJsonObject()// + .addProperty("voltageRatio", 0.93) // + .addProperty("percent", 0) // + .build()) // + .add(JsonUtils.buildJsonObject()// + .addProperty("voltageRatio", 1.07) // + .addProperty("percent", 0) // + .build()) // + .add(JsonUtils.buildJsonObject()// + .addProperty("voltageRatio", 1.1) // + .addProperty("percent", -60) // + .build() // + ).build().toString() // + ).build()) // + .next(new TestCase("First Input") // + .input(METER_VOLTAGE, 240_000) // [mV] + .input(MAX_APPARENT_POWER, 10_000)) // [VA] + .next(new TestCase() // + .output(ESS_REACTIVE_POWER, 0))// + .next(new TestCase("Second Input") // + .timeleap(clock, 5, ChronoUnit.SECONDS)// + .input(METER_VOLTAGE, 216_000) // [mV] + .input(MAX_APPARENT_POWER, 10_000) // [VA] + .output(ESS_REACTIVE_POWER, 6000))// + .next(new TestCase("Third Input")// + .timeleap(clock, 5, ChronoUnit.SECONDS)// + .input(METER_VOLTAGE, 220_000) // [mV] + .input(MAX_APPARENT_POWER, 10_000) // [VA] + .output(ESS_REACTIVE_POWER, 2600))// + .next(new TestCase()// + .timeleap(clock, 5, ChronoUnit.SECONDS)// + .input(METER_VOLTAGE, 223_000) // [mV] + .input(MAX_APPARENT_POWER, 10_000) // [VA] + .output(ESS_REACTIVE_POWER, 100))// + .next(new TestCase()// + .timeleap(clock, 5, ChronoUnit.SECONDS)// + .input(METER_VOLTAGE, 223_200) // [mV] + .input(MAX_APPARENT_POWER, 10_000) // [VA] + .output(ESS_REACTIVE_POWER, 0))// + .next(new TestCase()// + .timeleap(clock, 5, ChronoUnit.SECONDS)// + .input(METER_VOLTAGE, 256_800) // [mV] + .input(MAX_APPARENT_POWER, 10_000) // [VA] + .output(ESS_REACTIVE_POWER, 0))// + .next(new TestCase()// + .timeleap(clock, 5, ChronoUnit.SECONDS)// + .input(METER_VOLTAGE, 260_000) // [mV] + .input(MAX_APPARENT_POWER, 10_000) // [VA] + .output(ESS_REACTIVE_POWER, -2600))// + .next(new TestCase()// + .timeleap(clock, 5, ChronoUnit.SECONDS)// + .input(METER_VOLTAGE, 264_000) // [mV] + .input(MAX_APPARENT_POWER, 10_000) // [VA] + .output(ESS_REACTIVE_POWER, -6000))// + ; + } +} diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/MyConfig.java b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/MyConfig.java new file mode 100644 index 00000000000..1d977e0a4f5 --- /dev/null +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/MyConfig.java @@ -0,0 +1,108 @@ +package io.openems.edge.controller.ess.reactivepowervoltagecharacteristic; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.utils.ConfigUtils; +import io.openems.edge.common.test.AbstractComponentConfig; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private String essId; + private String meterId; + private int nominalVoltage; + private int waitForHysteresis; + private String powerVoltConfig; + + private Builder() { + + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setEssId(String essId) { + this.essId = essId; + return this; + } + + public Builder setMeterId(String meterId) { + this.meterId = meterId; + return this; + } + + public Builder setNominalVoltage(int nominalVoltage) { + this.nominalVoltage = nominalVoltage; + return this; + } + + public Builder setPowerVoltConfig(String powerVoltConfig) throws OpenemsNamedException { + this.powerVoltConfig = powerVoltConfig; + return this; + } + + public Builder setWaitForHysteresis(int waitForHysteresis) throws OpenemsNamedException { + this.waitForHysteresis = waitForHysteresis; + return this; + } + + public MyConfig build() { + return new MyConfig(this); + } + + } + + /** + * Create a Config builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + private final Builder builder; + + private MyConfig(Builder builder) { + super(Config.class, builder.id); + this.builder = builder; + } + + @Override + public String ess_id() { + return this.builder.essId; + } + + @Override + public String meter_id() { + return this.builder.meterId; + } + + @Override + public String lineConfig() { + return this.builder.powerVoltConfig; + } + + @Override + public float nominalVoltage() { + return this.builder.nominalVoltage; + } + + @Override + public int waitForHysteresis() { + return this.builder.waitForHysteresis; + } + + @Override + public String ess_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.ess_id()); + } + + @Override + public String meter_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.meter_id()); + } +} \ No newline at end of file diff --git a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/readme.adoc b/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/readme.adoc deleted file mode 100644 index de738b1a0f4..00000000000 --- a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/readme.adoc +++ /dev/null @@ -1,5 +0,0 @@ -= Symmetric Reactive-Power Voltage-Characteristics - -Controls a symmetric energy storage system using a Q-by-U reference function. - -https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/symmetric/reactivepowervoltagecharacteristic/ReactivePowerVoltageCharacteristic.java b/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/symmetric/reactivepowervoltagecharacteristic/ReactivePowerVoltageCharacteristic.java deleted file mode 100644 index f67754bb815..00000000000 --- a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/symmetric/reactivepowervoltagecharacteristic/ReactivePowerVoltageCharacteristic.java +++ /dev/null @@ -1,153 +0,0 @@ -package io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic; - -import java.util.HashMap; -import java.util.Map; - -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 com.google.gson.JsonArray; -import com.google.gson.JsonElement; - -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; -import io.openems.common.utils.JsonUtils; -import io.openems.edge.common.channel.Doc; -import io.openems.edge.common.channel.value.Value; -import io.openems.edge.common.component.AbstractOpenemsComponent; -import io.openems.edge.common.component.OpenemsComponent; -import io.openems.edge.controller.api.Controller; -import io.openems.edge.ess.api.ManagedSymmetricEss; -import io.openems.edge.ess.power.api.Phase; -import io.openems.edge.ess.power.api.Pwr; -import io.openems.edge.ess.power.api.Relationship; -import io.openems.edge.meter.api.SymmetricMeter; - -@Designate(ocd = Config.class, factory = true) -@Component(// - name = "Controller.Symmetric.ReactivePowerVoltageCharacteristic", // - immediate = true, // - configurationPolicy = ConfigurationPolicy.REQUIRE // -) -public class ReactivePowerVoltageCharacteristic extends AbstractOpenemsComponent - implements Controller, OpenemsComponent { - - private final Map qCharacteristic = new HashMap<>(); - - /** - * nominal voltage in [mV]. - */ - private float nominalVoltage; - - private int power = 0; - - @Reference - protected ConfigurationAdmin cm; - - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) - private SymmetricMeter meter; - - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) - private ManagedSymmetricEss ess; - - public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - ; - private final Doc doc; - - private ChannelId(Doc doc) { - this.doc = doc; - } - - @Override - public Doc doc() { - return this.doc; - } - } - - public ReactivePowerVoltageCharacteristic() { - super(// - OpenemsComponent.ChannelId.values(), // - Controller.ChannelId.values(), // - ChannelId.values() // - ); - } - - @Activate - void activate(ComponentContext context, Config config) throws OpenemsNamedException { - super.activate(context, config.id(), config.alias(), config.enabled()); - if (OpenemsComponent.updateReferenceFilter(cm, this.servicePid(), "ess", config.ess_id())) { - return; - } - if (OpenemsComponent.updateReferenceFilter(cm, this.servicePid(), "meter", config.meter_id())) { - return; - } - - this.nominalVoltage = config.nominalVoltage() * 1000; - this.initialize(config.percentQ()); - } - - @Deactivate - protected void deactivate() { - super.deactivate(); - } - - /** - * Initialize the Q by U characteristics. - * - *

- * Parsing JSON then putting the point variables into qByUCharacteristicEquation - * - *

-	 * [
-	 *   { "voltage": 0.9, "percent" : 60 },
-	 *   { "voltage": 0.93, "percent": 0 },
-	 *   { "voltage": 1.07, "percent": 0 },
-	 *   { "voltage": 1.1, "percent": -60 }
-	 * ]
-	 * 
- * - * @param percentQ the configured Percent-by-Q values - * @throws OpenemsNamedException on error - */ - private void initialize(String percentQ) throws OpenemsNamedException { - try { - JsonArray jPercentQ = JsonUtils.getAsJsonArray(JsonUtils.parse(percentQ)); - for (JsonElement element : jPercentQ) { - float percent = JsonUtils.getAsFloat(element, "percent"); - float voltage = JsonUtils.getAsFloat(element, "voltage"); - this.qCharacteristic.put(voltage, percent); - } - } catch (NullPointerException e) { - throw new OpenemsException("Unable to set values [" + percentQ + "] " + e.getMessage()); - } - } - - @Override - public void run() throws OpenemsException { - float voltageRatio = this.meter.getVoltage().orElse(0) / this.nominalVoltage; - float valueOfLine = Utils.getValueOfLine(this.qCharacteristic, voltageRatio); - if (valueOfLine == 0) { - return; - } - - Value apparentPower = this.ess.getMaxApparentPower(); - if (!apparentPower.isDefined() || apparentPower.get() == 0) { - return; - } - - this.power = (int) (apparentPower.orElse(0) * valueOfLine); - int calculatedPower = ess.getPower().fitValueIntoMinMaxPower(this.id(), ess, Phase.ALL, Pwr.REACTIVE, - this.power); - this.ess.addPowerConstraintAndValidate("ReactivePowerVoltageCharacteristic", Phase.ALL, Pwr.REACTIVE, - Relationship.EQUALS, calculatedPower); - } -} diff --git a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/symmetric/reactivepowervoltagecharacteristic/Utils.java b/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/symmetric/reactivepowervoltagecharacteristic/Utils.java deleted file mode 100644 index 955c269d0a8..00000000000 --- a/io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/symmetric/reactivepowervoltagecharacteristic/Utils.java +++ /dev/null @@ -1,91 +0,0 @@ -package io.openems.edge.controller.symmetric.reactivepowervoltagecharacteristic; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.stream.Collectors; - -public class Utils { - public static float getValueOfLine(Map points, float voltageRatio) { - float x = voltageRatio; - List percentList = new ArrayList(points.values()); - List voltageList = new ArrayList(points.keySet()); - Collections.sort(voltageList, Collections.reverseOrder()); - Collections.sort(percentList, Collections.reverseOrder()); - // find to place of voltage ratio - Point smaller = getSmallerPoint(points, voltageRatio); - Point greater = getGreaterPoint(points, voltageRatio); - float m = (float) ((greater.y - smaller.y) / (greater.x - smaller.x)); - float t = (float) (smaller.y - m * smaller.x); - return m * x + t; - } - - public static Point getSmallerPoint(Map qCharacteristic, float voltageRatio) { - Point p; - int i = 0; - // bubble sort outer loop - qCharacteristic.put(voltageRatio, (float) 0); - Comparator> valueComparator = (e1, e2) -> e1.getKey().compareTo(e2.getKey()); - Map map = qCharacteristic.entrySet().stream().sorted(valueComparator) - .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); - List voltageList = new ArrayList(map.keySet()); - List percentList = new ArrayList(map.values()); - if (voltageList.get(i) != voltageRatio) { - i++; - } - if (i == 0) { - p = new Point(voltageList.get(0), percentList.get(0)); - return p; - } - p = new Point(voltageList.get(i - 1), percentList.get(i - 1)); - - qCharacteristic.remove(voltageRatio, (float) 0); - return p; - } - - public static Point getGreaterPoint(Map qCharacteristic, float voltageRatio) { - Point p; - int i = 0; - // bubble sort outer loop - // 0 random number, just to fill value - qCharacteristic.put(voltageRatio, (float) 0); - Comparator> valueComparator = (e1, e2) -> e1.getKey().compareTo(e2.getKey()); - Map map = qCharacteristic.entrySet().stream().sorted(valueComparator) - .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); - List voltageList = new ArrayList(map.keySet()); - List percentList = new ArrayList(map.values()); - if (voltageList.get(i) != voltageRatio) { - i++; - } - if (i > voltageList.size()) { - p = new Point(voltageList.get(voltageList.size() - 1), percentList.size() - 1); - return p; - } - p = new Point(voltageList.get(i + 1), percentList.get(i + 1)); - - qCharacteristic.remove(voltageRatio, (float) 0); - return p; - } - - protected static class Point { - private final float x; - private final float y; - - public Point(float x, float y) { - this.x = x; - this.y = y; - } - - public float getX() { - return x; - } - - public float getY() { - return y; - } - } -}