-
Notifications
You must be signed in to change notification settings - Fork 400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add timeframe controller to reach specified SoC in specified time frame #2694
base: develop
Are you sure you want to change the base?
Changes from 6 commits
7eb5b84
a5663fa
7938c87
19c5aae
2a09488
2ed40e5
f84458b
5ded228
1d8e6a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
FROM openjdk:21 | ||
|
||
WORKDIR /app | ||
COPY ./build/openems-edge.jar /app/openems-edge.jar | ||
|
||
CMD ["java", "-jar", "openems-edge.jar"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -85,6 +85,7 @@ | |
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.ess.standby',\ | ||
bnd.identity;id='io.openems.edge.controller.ess.timeframe',\ | ||
bnd.identity;id='io.openems.edge.controller.ess.timeofusetariff',\ | ||
bnd.identity;id='io.openems.edge.controller.evcs',\ | ||
bnd.identity;id='io.openems.edge.controller.evcs.fixactivepower',\ | ||
|
@@ -113,6 +114,7 @@ | |
bnd.identity;id='io.openems.edge.ess.core',\ | ||
bnd.identity;id='io.openems.edge.ess.fenecon.commercial40',\ | ||
bnd.identity;id='io.openems.edge.ess.generic',\ | ||
bnd.identity;id='io.openems.edge.ess.mr.gridcon',\ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. has been removed acutally: MR Gridcon: move obsolete bundle to archive |
||
bnd.identity;id='io.openems.edge.ess.samsung',\ | ||
bnd.identity;id='io.openems.edge.ess.sma',\ | ||
bnd.identity;id='io.openems.edge.evcs.alpitronic.hypercharger',\ | ||
|
@@ -244,6 +246,7 @@ | |
io.openems.edge.controller.ess.cycle;version=snapshot,\ | ||
io.openems.edge.controller.ess.delaycharge;version=snapshot,\ | ||
io.openems.edge.controller.ess.delayedselltogrid;version=snapshot,\ | ||
io.openems.edge.controller.ess.timeframe;version=snapshot,\ | ||
io.openems.edge.controller.ess.emergencycapacityreserve;version=snapshot,\ | ||
io.openems.edge.controller.ess.fixactivepower;version=snapshot,\ | ||
io.openems.edge.controller.ess.fixstateofcharge;version=snapshot,\ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please have a look at other .classpath files and change it accordingly. Also run "prepare-commit.sh" in tools Folder before creating a PR please as this fixes exactly this "Problems". Also always apply "Strg-Shift-F" and "Strg-Shift-O" if using Eclipse. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<classpath> | ||
<classpathentry kind="con" path="aQute.bnd.classpath.container"/> | ||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/> | ||
<classpathentry kind="src" output="bin" path="src"/> | ||
<classpathentry kind="src" output="bin_test" path="test"> | ||
<attributes> | ||
<attribute name="test" value="true"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="output" path="bin"/> | ||
</classpath> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/bin/ | ||
/bin_test/ | ||
/generated/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<projectDescription> | ||
<name>io.openems.edge.controller.ess.timeframe</name> | ||
<comment></comment> | ||
<projects> | ||
</projects> | ||
<buildSpec> | ||
<buildCommand> | ||
<name>org.eclipse.jdt.core.javabuilder</name> | ||
<arguments> | ||
</arguments> | ||
</buildCommand> | ||
<buildCommand> | ||
<name>bndtools.core.bndbuilder</name> | ||
<arguments> | ||
</arguments> | ||
</buildCommand> | ||
</buildSpec> | ||
<natures> | ||
<nature>org.eclipse.jdt.core.javanature</nature> | ||
<nature>bndtools.core.bndnature</nature> | ||
</natures> | ||
</projectDescription> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
eclipse.preferences.version=1 | ||
encoding/<project>=UTF-8 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
Bundle-Name: OpenEMS Edge Controller Symmetric Timeframe | ||
Bundle-Vendor: Felix Mumme | ||
Bundle-License: https://opensource.org/licenses/EPL-2.0 | ||
Bundle-Version: 0.1.0 | ||
|
||
-buildpath: \ | ||
${buildpath},\ | ||
io.openems.common,\ | ||
io.openems.edge.common,\ | ||
io.openems.edge.controller.api,\ | ||
io.openems.edge.ess.api,\ | ||
io.openems.edge.timedata.api,\ | ||
|
||
-testpath: \ | ||
${testpath} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
= Symmetric Timeframe | ||
|
||
Defines a timeframe to bring a symmetric energy storage system to a specified SoC. | ||
|
||
https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.controller.timeframe[Source Code icon:github[]] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package io.openems.edge.controller.ess.timeframe; | ||
|
||
import org.osgi.service.metatype.annotations.AttributeDefinition; | ||
import org.osgi.service.metatype.annotations.ObjectClassDefinition; | ||
|
||
import io.openems.edge.ess.power.api.Phase; | ||
import io.openems.edge.ess.power.api.Relationship; | ||
|
||
@ObjectClassDefinition(// | ||
name = "Controller Ess Timeframe", // | ||
description = "Defines a timeframe to bring a symmetric energy storage system to a specified SoC.") | ||
@interface Config { | ||
|
||
@AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") | ||
String id() default "ctrlTimeframe0"; | ||
|
||
@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 = "Mode", description = "Set the type of mode.") | ||
Mode mode() default Mode.MANUAL; | ||
|
||
@AttributeDefinition(name = "Ess-ID", description = "ID of Ess device.") | ||
String ess_id(); | ||
|
||
@AttributeDefinition(name = "Fallback ESS Capacity", description = "Capacity of the ESS in Wh. Serves as Fallback, if ESS capacity can not be read from ESS.", required = false) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why should the SoC should not be able to be read? It should be set in the Specific ESS i guess. Also if this is neccessary why is it not required ? If you could make required to "True" you could spare:
as then the Fallback would always be there. It would also be good to seperate the two cases e.g:
and provide user feedback (log) |
||
int ess_capacity() default 0; | ||
|
||
@AttributeDefinition(name = "Target SoC [%]", description = "SoC to reach within the specified timeframe") | ||
int targetSoC(); | ||
|
||
@AttributeDefinition(name = "Max charge power [W]", description = "Max power that can be used to charge the ESS. 0 means no limit.") | ||
int maxChargePower() default 0; | ||
|
||
@AttributeDefinition(name = "Max discharge power [W]", description = "Max power that can be used to discharge the ESS. 0 means no limit.") | ||
int maxDischargePower() default 0; | ||
|
||
@AttributeDefinition(name = "Start Time", description = "Start of the timeframe used in manual mode; ISO 8601 format") | ||
String startTime(); | ||
|
||
@AttributeDefinition(name = "End Time", description = "End of the timeframe used in manual mode; ISO 8601 format") | ||
String endTime(); | ||
|
||
@AttributeDefinition(name = "Power Relationship", description = "Target power must be equal, less-than or greater-than the configured power value") | ||
Relationship relationship() default Relationship.EQUALS; | ||
|
||
@AttributeDefinition(name = "Phase", description = "Apply target power to L1, L2, L3 or sum of all phases") | ||
Phase phase() default Phase.ALL; | ||
|
||
String webconsole_configurationFactory_nameHint() default "Controller Ess Timeframe [{id}]"; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package io.openems.edge.controller.ess.timeframe; | ||
|
||
import static io.openems.common.channel.PersistencePriority.HIGH; | ||
import static io.openems.common.channel.Unit.CUMULATED_SECONDS; | ||
import static io.openems.common.types.OpenemsType.LONG; | ||
|
||
import io.openems.edge.common.channel.Doc; | ||
import io.openems.edge.common.component.OpenemsComponent; | ||
import io.openems.edge.controller.api.Controller; | ||
|
||
public interface ControllerEssTimeframe extends Controller, OpenemsComponent { | ||
|
||
public enum ChannelId implements io.openems.edge.common.channel.ChannelId { | ||
CUMULATED_ACTIVE_TIME(Doc.of(LONG)// | ||
.unit(CUMULATED_SECONDS) // | ||
.persistencePriority(HIGH)); | ||
|
||
private final Doc doc; | ||
|
||
private ChannelId(Doc doc) { | ||
this.doc = doc; | ||
} | ||
|
||
@Override | ||
public Doc doc() { | ||
return this.doc; | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
package io.openems.edge.controller.ess.timeframe; | ||
|
||
import io.openems.common.exceptions.InvalidValueException; | ||
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.Modified; | ||
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 io.openems.common.exceptions.OpenemsError.OpenemsNamedException; | ||
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.api.PowerConstraint; | ||
import io.openems.edge.ess.power.api.Pwr; | ||
import io.openems.edge.timedata.api.Timedata; | ||
import io.openems.edge.timedata.api.TimedataProvider; | ||
import io.openems.edge.timedata.api.utils.CalculateActiveTime; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.time.Instant; | ||
import java.time.OffsetDateTime; | ||
import java.time.format.DateTimeFormatter; | ||
import java.util.Date; | ||
|
||
@Designate(ocd = Config.class, factory = true) | ||
@Component(// | ||
name = "Controller.Ess.Timeframe", // | ||
immediate = true, // | ||
configurationPolicy = ConfigurationPolicy.REQUIRE // | ||
) | ||
public class ControllerEssTimeframeImpl extends AbstractOpenemsComponent | ||
implements ControllerEssTimeframe, Controller, OpenemsComponent, TimedataProvider { | ||
|
||
private final CalculateActiveTime calculateCumulatedActiveTime = new CalculateActiveTime(this, | ||
ControllerEssTimeframe.ChannelId.CUMULATED_ACTIVE_TIME); | ||
|
||
@Reference | ||
private ConfigurationAdmin cm; | ||
|
||
@Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) | ||
private ManagedSymmetricEss ess; | ||
|
||
private Config config; | ||
|
||
@Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) | ||
private volatile Timedata timedata = null; | ||
|
||
|
||
private final Logger log = LoggerFactory.getLogger(io.openems.edge.controller.ess.timeframe.ControllerEssTimeframeImpl.class); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This Line could be Changed to:
Also it is not used in the Code as far as i can see |
||
|
||
public ControllerEssTimeframeImpl() { | ||
super(// | ||
OpenemsComponent.ChannelId.values(), // | ||
Controller.ChannelId.values(), // | ||
ControllerEssTimeframe.ChannelId.values() // | ||
); | ||
} | ||
|
||
@Activate | ||
private void activate(ComponentContext context, Config config) { | ||
super.activate(context, config.id(), config.alias(), config.enabled()); | ||
if (this.applyConfig(context, config)) { | ||
return; | ||
} | ||
} | ||
|
||
@Modified | ||
private void modified(ComponentContext context, Config config) { | ||
super.modified(context, config.id(), config.alias(), config.enabled()); | ||
if (this.applyConfig(context, config)) { | ||
return; | ||
} | ||
} | ||
|
||
private boolean applyConfig(ComponentContext context, Config config) { | ||
this.config = config; | ||
return OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "ess", config.ess_id()); | ||
} | ||
|
||
@Override | ||
@Deactivate | ||
protected void deactivate() { | ||
super.deactivate(); | ||
} | ||
|
||
@Override | ||
public void run() throws OpenemsNamedException { | ||
var isActive = false; | ||
try { | ||
isActive = switch (this.config.mode()) { | ||
case MANUAL -> { | ||
// Apply Active-Power Set-Point | ||
var acPower = getAcPower( | ||
this.ess, | ||
this.config.ess_capacity(), | ||
this.config.targetSoC(), | ||
this.config.maxChargePower(), | ||
this.config.maxDischargePower(), | ||
this.config.startTime(), | ||
this.config.endTime() | ||
); | ||
|
||
if (acPower == null) { | ||
yield false; // is not active | ||
} else { | ||
PowerConstraint.apply(this.ess, this.id(), // | ||
this.config.phase(), Pwr.ACTIVE, this.config.relationship(), acPower); | ||
yield true; // is active | ||
} | ||
} | ||
case OFF -> { | ||
// Do nothing | ||
yield false; // is not active | ||
} | ||
}; | ||
|
||
} finally { | ||
this.calculateCumulatedActiveTime.update(isActive); | ||
} | ||
} | ||
|
||
/** | ||
* Gets the required AC power set-point for AC-ESS. | ||
* | ||
* @param ess the {@link ManagedSymmetricEss} | ||
* @param fallback_ess_capacity capacity of the ess, used as fallback if automatic determination does not work | ||
* @param targetSoC the target SoC | ||
* @param maxChargePower max power to charge | ||
* @param maxDischargePower max power to discharge | ||
* @param startTime the start time of the timeframe | ||
* @param endTime the end time of the timeframe | ||
* @return the AC power set-point | ||
*/ | ||
protected static Integer getAcPower( | ||
ManagedSymmetricEss ess, | ||
Integer fallback_ess_capacity, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fallbackEssCapacity - please watch or keep the Formatting of the Variables as in the other Code |
||
int targetSoC, | ||
int maxChargePower, | ||
int maxDischargePower, | ||
String startTime, | ||
String endTime | ||
) throws InvalidValueException { | ||
Date start = getDateFromIsoString(startTime); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of using Date, the method could work directly with Instant, which is more modern and straightforward for handling time-based logic.
Then you also can get rid of: getDatefromIsoString method |
||
Date end = getDateFromIsoString(endTime); | ||
Date current = new Date(); | ||
if (current.after(start) && current.before(end)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The check for whether the current time is within the start and end times should be done early, allowing for an immediate return if the ESS is not active. This reduces the complexity of the code by avoiding unnecessary nesting. |
||
int currentSoC = ess.getSoc().getOrError(); | ||
Integer maxEssCapacity = ess.getCapacity().orElse(fallback_ess_capacity); | ||
|
||
if (maxEssCapacity == null || maxEssCapacity <= 0) { | ||
throw new InvalidValueException("could not determine ESS capacity and no valid fallback capacity was configured."); | ||
} | ||
|
||
int targetCapacity = maxEssCapacity * targetSoC / 100; | ||
int currentCapacity = maxEssCapacity * currentSoC / 100; | ||
|
||
float timeframeInHours = (end.getTime() - current.getTime()) / 1000f / 60f / 60f; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe use a private static final for the Milliseconds to Hours:
|
||
int requestedPower = Math.round((currentCapacity - targetCapacity) / timeframeInHours); | ||
if (maxChargePower != 0) { | ||
requestedPower = Math.max(requestedPower, -maxChargePower); | ||
} | ||
if (maxDischargePower != 0) { | ||
requestedPower = Math.min(requestedPower, maxDischargePower); | ||
} | ||
|
||
return requestedPower; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
private static Date getDateFromIsoString(String iso8601String) { | ||
DateTimeFormatter timeFormatter = DateTimeFormatter.ISO_DATE_TIME; | ||
OffsetDateTime offsetDateTime = OffsetDateTime.parse(iso8601String, timeFormatter); | ||
|
||
return Date.from(Instant.from(offsetDateTime)); | ||
} | ||
|
||
@Override | ||
public Timedata getTimedata() { | ||
return this.timedata; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package io.openems.edge.controller.ess.timeframe; | ||
|
||
public enum Mode { | ||
MANUAL, OFF; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess "ON", "AUTO", "OFF" would be better as "ON" charges as it should do as configured. etc. |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this file should not be in a PR.