Skip to content
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

Draft
wants to merge 9 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM openjdk:21
Copy link
Contributor

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.


WORKDIR /app
COPY ./build/openems-edge.jar /app/openems-edge.jar

CMD ["java", "-jar", "openems-edge.jar"]
3 changes: 3 additions & 0 deletions io.openems.edge.application/EdgeApp.bndrun
Original file line number Diff line number Diff line change
Expand Up @@ -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',\
Expand Down Expand Up @@ -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',\
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has been removed acutally:

MR Gridcon: move obsolete bundle to archive
See https://github.com/fenecon/openems-archive/tree/main/io.openems.edge.ess.mr.gridcon

#2727

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',\
Expand Down Expand Up @@ -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,\
Expand Down
12 changes: 12 additions & 0 deletions io.openems.edge.controller.ess.timeframe/.classpath
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
3 changes: 3 additions & 0 deletions io.openems.edge.controller.ess.timeframe/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/bin/
/bin_test/
/generated/
23 changes: 23 additions & 0 deletions io.openems.edge.controller.ess.timeframe/.project
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
15 changes: 15 additions & 0 deletions io.openems.edge.controller.ess.timeframe/bnd.bnd
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}
5 changes: 5 additions & 0 deletions io.openems.edge.controller.ess.timeframe/readme.adoc
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)
Copy link
Contributor

@Sn0w3y Sn0w3y Aug 19, 2024

Choose a reason for hiding this comment

The 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:

if (essCapacity <= 0) { throw new InvalidValueException( "Could not determine ESS capacity and no valid fallback capacity was configured."); }

as then the Fallback would always be there.

It would also be good to seperate the two cases e.g:

  1. ESS Capacity not readable and no Fallback configured
  2. ESS Capacity not readable using Fallback

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Line could be Changed to:

private final Logger log = LoggerFactory
			.getLogger(ControllerEssTimeframeImpl.class); 

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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Instant currentInstant = Instant.now();
Instant startInstant = OffsetDateTime.parse(startTime, DateTimeFormatter.ISO_DATE_TIME).toInstant();
Instant endInstant = OffsetDateTime.parse(endTime, DateTimeFormatter.ISO_DATE_TIME).toInstant();

Then you also can get rid of:

getDatefromIsoString method

Date end = getDateFromIsoString(endTime);
Date current = new Date();
if (current.after(start) && current.before(end)) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use a private static final for the Milliseconds to Hours:

private static final float MILLISECONDS_TO_HOURS = 1000f / 60f / 60f;

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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

}
Empty file.
Loading