diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..77424d65c05 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM openjdk:21 + +WORKDIR /app +COPY ./build/openems-edge.jar /app/openems-edge.jar + +CMD ["java", "-jar", "openems-edge.jar"] \ No newline at end of file diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index 4dfeab59531..be3bee73c9c 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -74,7 +74,6 @@ 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.emergencycapacityreserve',\ - bnd.identity;id='io.openems.edge.controller.ess.fastfrequencyreserve',\ bnd.identity;id='io.openems.edge.controller.ess.fixactivepower',\ bnd.identity;id='io.openems.edge.controller.ess.fixstateofcharge',\ bnd.identity;id='io.openems.edge.controller.ess.gridoptimizedcharge',\ @@ -86,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',\ @@ -245,8 +245,8 @@ 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.fastfrequencyreserve;version=snapshot,\ io.openems.edge.controller.ess.fixactivepower;version=snapshot,\ io.openems.edge.controller.ess.fixstateofcharge;version=snapshot,\ io.openems.edge.controller.ess.gridoptimizedcharge;version=snapshot,\ diff --git a/io.openems.edge.controller.ess.timeframe/.classpath b/io.openems.edge.controller.ess.timeframe/.classpath new file mode 100644 index 00000000000..bbfbdbe40e7 --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/io.openems.edge.controller.ess.timeframe/.gitignore b/io.openems.edge.controller.ess.timeframe/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/io.openems.edge.controller.ess.timeframe/.project b/io.openems.edge.controller.ess.timeframe/.project new file mode 100644 index 00000000000..8df9ba9150b --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/.project @@ -0,0 +1,23 @@ + + + io.openems.edge.controller.ess.timeframe + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.edge.controller.ess.timeframe/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.controller.ess.timeframe/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/io.openems.edge.controller.ess.timeframe/bnd.bnd b/io.openems.edge.controller.ess.timeframe/bnd.bnd new file mode 100644 index 00000000000..b779a97a16c --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/bnd.bnd @@ -0,0 +1,16 @@ +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,\ + io.openems.edge.meter.api + +-testpath: \ + ${testpath} diff --git a/io.openems.edge.controller.ess.timeframe/readme.adoc b/io.openems.edge.controller.ess.timeframe/readme.adoc new file mode 100644 index 00000000000..9e7bae923f5 --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/readme.adoc @@ -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[]] \ No newline at end of file diff --git a/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/Config.java b/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/Config.java new file mode 100644 index 00000000000..e5506671c49 --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/Config.java @@ -0,0 +1,60 @@ +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 = "Grid Meter-ID", description = "ID of grid meter.", required = false) + String grid_meter_id() default ""; + + @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) + 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 = "Max buy from grid power [W]", description = "Max power that will be drawn from grid to charge the ESS. 0 means no limit.") + int maxBuyFromGridPower() 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}]"; +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/ControllerEssTimeframe.java b/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/ControllerEssTimeframe.java new file mode 100644 index 00000000000..a1f4e3c37bf --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/ControllerEssTimeframe.java @@ -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; + } + } + +} diff --git a/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/ControllerEssTimeframeImpl.java b/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/ControllerEssTimeframeImpl.java new file mode 100644 index 00000000000..915004d1cf6 --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/ControllerEssTimeframeImpl.java @@ -0,0 +1,210 @@ +package io.openems.edge.controller.ess.timeframe; + +import io.openems.common.exceptions.InvalidValueException; +import io.openems.edge.meter.api.ElectricityMeter; +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; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) + private ElectricityMeter meter; + + 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); + + 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; + OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "meter", config.grid_meter_id()); + 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.meter, + this.config.ess_capacity(), + this.config.targetSoC(), + this.config.maxChargePower(), + this.config.maxDischargePower(), + this.config.maxBuyFromGridPower(), + 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 meter the {@link ElectricityMeter} + * @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 maxBuyFromGridPower max power to buy from grid + * @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, + ElectricityMeter meter, + Integer fallback_ess_capacity, + int targetSoC, + int maxChargePower, + int maxDischargePower, + int maxBuyFromGridPower, + String startTime, + String endTime + ) throws InvalidValueException { + Date start = getDateFromIsoString(startTime); + Date end = getDateFromIsoString(endTime); + Date current = new Date(); + if (current.after(start) && current.before(end)) { + 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; + int requestedPower = Math.round((currentCapacity - targetCapacity) / timeframeInHours); + if (maxChargePower != 0) { + requestedPower = Math.max(requestedPower, -maxChargePower); + } + if (maxDischargePower != 0) { + requestedPower = Math.min(requestedPower, maxDischargePower); + } + + if (meter != null && maxBuyFromGridPower != 0) { + int gridPower = meter.getActivePower().getOrError(); + int maxCharge = ess.getActivePower().get() + gridPower - maxBuyFromGridPower; + requestedPower = Math.max(requestedPower, maxCharge); + } + + 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; + } +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/Mode.java b/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/Mode.java new file mode 100644 index 00000000000..e29a9139b99 --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/src/io/openems/edge/controller/ess/timeframe/Mode.java @@ -0,0 +1,5 @@ +package io.openems.edge.controller.ess.timeframe; + +public enum Mode { + MANUAL, OFF; +} diff --git a/io.openems.edge.controller.ess.timeframe/test/.gitignore b/io.openems.edge.controller.ess.timeframe/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/io.openems.edge.controller.ess.timeframe/test/io/openems/edge/controller/ess/timeframe/ControllerEssTimeframeImplTest.java b/io.openems.edge.controller.ess.timeframe/test/io/openems/edge/controller/ess/timeframe/ControllerEssTimeframeImplTest.java new file mode 100644 index 00000000000..24a35354fe0 --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/test/io/openems/edge/controller/ess/timeframe/ControllerEssTimeframeImplTest.java @@ -0,0 +1,151 @@ +package io.openems.edge.controller.ess.timeframe; + +import io.openems.common.exceptions.InvalidValueException; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import org.junit.Test; + +import io.openems.common.exceptions.OpenemsException; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.power.api.Phase; +import io.openems.edge.ess.power.api.Relationship; +import io.openems.edge.ess.test.DummyManagedAsymmetricEss; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + + +public class ControllerEssTimeframeImplTest { + + private static final String CTRL_ID = "ctrl0"; + private static final String ESS_ID = "ess0"; + + @Test + public void testManual() throws OpenemsException, Exception { + final var ess = new DummyManagedAsymmetricEss(ESS_ID); + new ControllerTest(new ControllerEssTimeframeImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("ess", ess) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMode(Mode.MANUAL) // + .setStartTime("2021-01-01T00:00:00Z") // + .setEndTime("2021-01-01T01:00:00Z") // + .setTargetSoC(50) // + .setPhase(Phase.ALL) // + .setRelationship(Relationship.EQUALS) // + .build()); // + } + + @Test + public void testOff() throws OpenemsException, Exception { + new ControllerTest(new ControllerEssTimeframeImpl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("ess", new DummyManagedAsymmetricEss(ESS_ID)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMode(Mode.OFF) // + .setStartTime("2021-01-01T00:00:00Z") // + .setEndTime("2021-01-01T01:00:00Z") // + .setTargetSoC(50) // + .setPhase(Phase.ALL) // + .setRelationship(Relationship.EQUALS) // + .build()); // + } + + @Test + public void testLimitChargePower() throws InvalidValueException { + + var ess = new DummyManagedSymmetricEss(ESS_ID) + .withCapacity(10000) + .withSoc(25) + .withActivePower(0) + .withMaxApparentPower(10000); + + assertEquals(Integer.valueOf(-1000), // + ControllerEssTimeframeImpl.getAcPower( + ess, + 0, + 50, + 1000, + 0, + this.getIso8601String(this.now()), + this.getIso8601String(this.inOneHour()) + )); + + + Integer acPower = ControllerEssTimeframeImpl.getAcPower( + ess, + 0, + 50, + 0, + 0, + this.getIso8601String(this.now()), + this.getIso8601String(this.inOneHour()) + ); + assertNotNull(acPower); + assertTrue(acPower < -1000); + + } + + @Test + public void testLimitDischargePower() throws InvalidValueException { + + var ess = new DummyManagedSymmetricEss(ESS_ID) + .withCapacity(10000) + .withSoc(75) + .withActivePower(0) + .withMaxApparentPower(10000); + + assertEquals(Integer.valueOf(1000), // + ControllerEssTimeframeImpl.getAcPower( + ess, + 0, + 50, + 0, + 1000, + this.getIso8601String(this.now()), + this.getIso8601String(this.inOneHour()) + )); + + + Integer acPower = ControllerEssTimeframeImpl.getAcPower( + ess, + null, + 0, + 50, + 0, + 0, + 0, + this.getIso8601String(this.now()), + this.getIso8601String(this.inOneHour()) + ); + assertNotNull(acPower); + assertTrue(acPower > 1000); + + } + + + private Date now() { + return new Date(); + } + + private Date inOneHour() { + return new Date(this.now().getTime() + 3600 * 1000); + } + + + private String getIso8601String(Date date) { + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + return df.format(date); + } +} diff --git a/io.openems.edge.controller.ess.timeframe/test/io/openems/edge/controller/ess/timeframe/MyConfig.java b/io.openems.edge.controller.ess.timeframe/test/io/openems/edge/controller/ess/timeframe/MyConfig.java new file mode 100644 index 00000000000..4c927e21e6f --- /dev/null +++ b/io.openems.edge.controller.ess.timeframe/test/io/openems/edge/controller/ess/timeframe/MyConfig.java @@ -0,0 +1,155 @@ +package io.openems.edge.controller.ess.timeframe; + +import io.openems.common.test.AbstractComponentConfig; +import io.openems.edge.ess.power.api.Phase; +import io.openems.edge.ess.power.api.Relationship; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private String essId; + private Mode mode; + private Phase phase; + private Relationship relationship; + + private int essCapacity; + private int targetSoC; + private int maxChargePower; + private int maxDischargePower; + private String startTime; + private String endTime; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setEssId(String essId) { + this.essId = essId; + return this; + } + + public Builder setMode(Mode mode) { + this.mode = mode; + return this; + } + + public Builder setPhase(Phase phase) { + this.phase = phase; + return this; + } + + public Builder setRelationship(Relationship relationship) { + this.relationship = relationship; + return this; + } + + public Builder setEss_capacity(int essCapacity) { + this.essCapacity = essCapacity; + return this; + } + + public Builder setTargetSoC(int targetSoC) { + this.targetSoC = targetSoC; + return this; + } + + public Builder setStartTime(String startTime) { + this.startTime = startTime; + return this; + } + + public Builder setMaxChargePower(int maxChargePower) { + this.maxChargePower = maxChargePower; + return this; + } + + public Builder setMaxDishargePower(int maxDischargePower) { + this.maxDischargePower = maxDischargePower; + return this; + } + + + public Builder setEndTime(String endTime) { + this.endTime = endTime; + 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 int ess_capacity() { + return this.builder.essCapacity; + } + + @Override + public int targetSoC() { + return this.builder.targetSoC; + } + + @Override + public int maxChargePower() { + return this.builder.maxChargePower; + } + + @Override + public int maxDischargePower() { + return this.builder.maxDischargePower; + } + + @Override + public String startTime() { + return this.builder.startTime; + } + + @Override + public String endTime() { + return this.builder.endTime; + } + + @Override + public Mode mode() { + return this.builder.mode; + } + + + @Override + public Relationship relationship() { + return this.builder.relationship; + } + + @Override + public Phase phase() { + return this.builder.phase; + } + +} \ No newline at end of file diff --git a/ui/src/app/edge/live/Controller/Ess/Timeframe/Ess_Timeframe.ts b/ui/src/app/edge/live/Controller/Ess/Timeframe/Ess_Timeframe.ts new file mode 100644 index 00000000000..488f0f2ccec --- /dev/null +++ b/ui/src/app/edge/live/Controller/Ess/Timeframe/Ess_Timeframe.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { SharedModule } from 'src/app/shared/shared.module'; +import { FlatComponent } from './flat/flat'; +import { ModalComponent } from './modal/modal'; + +@NgModule({ + imports: [ + BrowserModule, + SharedModule, + ], + declarations: [ + FlatComponent, + ModalComponent, + ], + exports: [ + FlatComponent, + ], +}) +export class Controller_Ess_Timeframe { } + + diff --git a/ui/src/app/edge/live/Controller/Ess/Timeframe/flat/flat.html b/ui/src/app/edge/live/Controller/Ess/Timeframe/flat/flat.html new file mode 100644 index 00000000000..24c750798bd --- /dev/null +++ b/ui/src/app/edge/live/Controller/Ess/Timeframe/flat/flat.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/ui/src/app/edge/live/Controller/Ess/Timeframe/flat/flat.ts b/ui/src/app/edge/live/Controller/Ess/Timeframe/flat/flat.ts new file mode 100644 index 00000000000..ab3c0aea5ad --- /dev/null +++ b/ui/src/app/edge/live/Controller/Ess/Timeframe/flat/flat.ts @@ -0,0 +1,54 @@ +// @ts-strict-ignore +import {Component} from '@angular/core'; +import { AbstractFlatWidget } from 'src/app/shared/components/flat/abstract-flat-widget'; +import {ChannelAddress, CurrentData, Utils} from 'src/app/shared/shared'; + +import {ModalComponent} from '../modal/modal'; + +@Component({ + selector: 'Controller_Ess_Timeframe', + templateUrl: './flat.html', +}) +export class FlatComponent extends AbstractFlatWidget { + + public targetSoC: number; + public endTime: string; + public startTime: string; + public propertyMode: string; + + public readonly CONVERT_TO_PERCENT = Utils.CONVERT_TO_PERCENT; + public readonly CONVERT_MANUAL_AUTO_OFF = Utils.CONVERT_MANUAL_AUTO_OFF(this.translate); + + async presentModal() { + if (!this.isInitialized) { + return; + } + const modal = await this.modalController.create({ + component: ModalComponent, + componentProps: { + component: this.component, + }, + }); + return await modal.present(); + } + + protected override getChannelAddresses(): ChannelAddress[] { + return [ + new ChannelAddress(this.component.id, "_PropertyTargetSoC"), + new ChannelAddress(this.component.id, "_PropertyMode"), + new ChannelAddress(this.component.id, "_PropertyStartTime"), + new ChannelAddress(this.component.id, "_PropertyEndTime"), + ]; + } + + protected override onCurrentData(currentData: CurrentData) { + this.targetSoC = currentData.allComponents[this.component.id + '/_PropertyTargetSoC']; + + const start = currentData.allComponents[this.component.id + '/_PropertyStartTime']; + const end = currentData.allComponents[this.component.id + '/_PropertyEndTime']; + + this.startTime = start ? Utils.CONVERT_DATE(start) : '-'; + this.endTime = end ? Utils.CONVERT_DATE(end) : '-'; + this.propertyMode = currentData.allComponents[this.component.id + '/_PropertyMode']; + } +} diff --git a/ui/src/app/edge/live/Controller/Ess/Timeframe/modal/modal.html b/ui/src/app/edge/live/Controller/Ess/Timeframe/modal/modal.html new file mode 100644 index 00000000000..a433f445001 --- /dev/null +++ b/ui/src/app/edge/live/Controller/Ess/Timeframe/modal/modal.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/app/edge/live/Controller/Ess/Timeframe/modal/modal.ts b/ui/src/app/edge/live/Controller/Ess/Timeframe/modal/modal.ts new file mode 100644 index 00000000000..fec7e96adcd --- /dev/null +++ b/ui/src/app/edge/live/Controller/Ess/Timeframe/modal/modal.ts @@ -0,0 +1,48 @@ +// @ts-strict-ignore +import {Component} from '@angular/core'; +import {FormControl, FormGroup, Validators} from '@angular/forms'; +import { AbstractModal } from 'src/app/shared/components/modal/abstractModal'; +import {ChannelAddress, CurrentData, Utils} from 'src/app/shared/shared'; + +@Component({ + templateUrl: './modal.html', +}) +export class ModalComponent extends AbstractModal { + + public targetSoC: number; + public endTime: string; + public startTime: string; + public propertyMode: string; + + public readonly CONVERT_TO_PERCENT = Utils.CONVERT_TO_PERCENT; + public readonly CONVERT_MANUAL_AUTO_OFF = Utils.CONVERT_MANUAL_AUTO_OFF(this.translate); + + protected override getChannelAddresses(): ChannelAddress[] { + return [ + new ChannelAddress(this.component.id, "_PropertyTargetSoC"), + new ChannelAddress(this.component.id, "_PropertyMode"), + new ChannelAddress(this.component.id, "_PropertyStartTime"), + new ChannelAddress(this.component.id, "_PropertyEndTime"), + ]; + } + + protected override onCurrentData(currentData: CurrentData) { + this.targetSoC = currentData.allComponents[this.component.id + '/_PropertyTargetSoC']; + + const start = currentData.allComponents[this.component.id + '/_PropertyStartTime']; + const end = currentData.allComponents[this.component.id + '/_PropertyEndTime']; + + this.startTime = start ? Utils.CONVERT_DATE(start) : '-'; + this.endTime = end ? Utils.CONVERT_DATE(end) : '-'; + this.propertyMode = currentData.allComponents[this.component.id + '/_PropertyMode'] ?? 'OFF'; + } + + protected override getFormGroup(): FormGroup { + return this.formBuilder.group({ + mode: new FormControl(this.component.properties.mode), + targetSoC: new FormControl(this.component.properties.targetSoC, [Validators.required, Validators.min(0), Validators.max(100)]), + startTime: new FormControl(this.component.properties.startTime), + endTime: new FormControl(this.component.properties.endTime), + }); + } +} diff --git a/ui/src/app/edge/live/live.component.html b/ui/src/app/edge/live/live.component.html index 9d4cf55d172..2d50a5dfa04 100644 --- a/ui/src/app/edge/live/live.component.html +++ b/ui/src/app/edge/live/live.component.html @@ -139,6 +139,10 @@ + + + + diff --git a/ui/src/app/edge/live/live.module.ts b/ui/src/app/edge/live/live.module.ts index ffec734d90c..ea1799ad16d 100644 --- a/ui/src/app/edge/live/live.module.ts +++ b/ui/src/app/edge/live/live.module.ts @@ -13,6 +13,7 @@ import { StorageComponent } from "./common/storage/storage.component"; import { Controller_ChannelthresholdComponent } from "./Controller/Channelthreshold/Channelthreshold"; import { Controller_ChpSocComponent } from "./Controller/ChpSoc/ChpSoc"; import { Controller_ChpSocModalComponent } from "./Controller/ChpSoc/modal/modal.component"; +import {Controller_Ess_Timeframe} from "./Controller/Ess/Timeframe/Ess_Timeframe"; import { Controller_Ess_FixActivePower } from "./Controller/Ess/FixActivePower/Ess_FixActivePower"; import { Controller_Ess_GridOptimizedCharge } from "./Controller/Ess/GridOptimizedCharge/Ess_GridOptimizedCharge"; import { Controller_Ess_TimeOfUseTariff } from "./Controller/Ess/TimeOfUseTariff/Ess_TimeOfUseTariff"; @@ -54,6 +55,7 @@ import { Evcs_Api_ClusterModalComponent } from "./Multiple/Evcs_Api_Cluster/moda Common_Selfconsumption, Controller_Api_ModbusTcp, Controller_Ess_FixActivePower, + Controller_Ess_Timeframe, Controller_Ess_GridOptimizedCharge, Controller_Ess_TimeOfUseTariff, Controller_Evcs, diff --git a/ui/src/app/shared/components/modal/modal-line/modal-line.html b/ui/src/app/shared/components/modal/modal-line/modal-line.html index c75997960ec..fd7cec3de92 100644 --- a/ui/src/app/shared/components/modal/modal-line/modal-line.html +++ b/ui/src/app/shared/components/modal/modal-line/modal-line.html @@ -1,23 +1,23 @@ - + - - + + - - + + - + + @@ -38,45 +38,65 @@ - - + + {{ option.name }} + + + + + + - + - - - - - + + + + + - - - + + +
- {{ displayName }} - + {{ displayName }} + - - {{ displayValue }} - + + {{ displayValue }} + - - + + - - - - + + + + - - {{option.name}} - - - - + + General.onDateAtTime + + + + + + + + - - -
+ + +
- - - - {{ control.properties.min | unitvalue:control.properties.unit}} - - - {{ control.properties.max | unitvalue:control.properties.unit}} - - - -
+ + + + {{ control.properties.min | unitvalue:control.properties.unit }} + + + {{ control.properties.max | unitvalue:control.properties.unit }} + + + +
diff --git a/ui/src/app/shared/components/modal/modal-line/modal-line.ts b/ui/src/app/shared/components/modal/modal-line/modal-line.ts index 76e9e187946..cb71bf0c6c4 100644 --- a/ui/src/app/shared/components/modal/modal-line/modal-line.ts +++ b/ui/src/app/shared/components/modal/modal-line/modal-line.ts @@ -1,35 +1,39 @@ -import { Component, Input } from "@angular/core"; -import { AbstractModalLine } from "../abstract-modal-line"; -import { ButtonLabel } from "../modal-button/modal-button"; +import {Component, Input} from "@angular/core"; +import {AbstractModalLine} from "../abstract-modal-line"; +import {ButtonLabel} from "../modal-button/modal-button"; @Component({ - selector: "oe-modal-line", - templateUrl: "./modal-line.html", + selector: "oe-modal-line", + templateUrl: "./modal-line.html", }) export class ModalLineComponent extends AbstractModalLine { - /** ControlName for Form Field */ + /** ControlName for Form Field */ @Input({ required: true }) public override controlName!: string; // Width of Left Column, Right Column is (100% - leftColumn) - @Input({ required: true }) protected leftColumnWidth!: number; + @Input({ required: true }) + protected leftColumnWidth!: number; - @Input() protected button: ButtonLabel | null = null; - /** ControlName for Toggle Button */ - @Input({ required: true }) protected control!: - { type: "TOGGLE" } | - { type: "INPUT", properties?: { unit: "W" } } | - /* the available select options*/ - { type: "SELECT", options: { value: string, name: string }[] } | - /* the properties for range slider*/ - { type: "RANGE", properties: { min: number, max: number, unit: "H", step?: number } }; - /** Fixed indentation of the modal-line */ - @Input() protected textIndent: TextIndentation = TextIndentation.NONE; + @Input() protected button: ButtonLabel | null = null; + /** ControlName for Toggle Button */ + @Input({ required: true }) protected control!: + { type: 'TOGGLE' } | + { type: 'INPUT', properties?: { unit: 'W' | '%' } } | + /* the available select options*/ + { type: 'SELECT', options: { value: string, name: string }[] } | + /* the properties for range slider*/ + { type: 'RANGE', properties: { min: number, max: number, unit: 'H' | '%', step?: number } } | + /* the properties for range slider*/ + { type: 'DATE_PICKER', properties?: { label: string } }; + + /** Fixed indentation of the modal-line */ + @Input() protected textIndent: TextIndentation = TextIndentation.NONE; } export enum TextIndentation { - NONE = "0%", - SINGLE = "5%", - DOUBLE = "10%", + NONE = "0%", + SINGLE = "5%", + DOUBLE = "10%", } diff --git a/ui/src/app/shared/service/defaulttypes.ts b/ui/src/app/shared/service/defaulttypes.ts index f9652fb6dbe..e119e1d2b64 100644 --- a/ui/src/app/shared/service/defaulttypes.ts +++ b/ui/src/app/shared/service/defaulttypes.ts @@ -14,6 +14,7 @@ export namespace DefaultTypes { } export type ManualOnOff = "MANUAL_ON" | "MANUAL_OFF"; + export type ManualOffAuto = 'MANUAL' | 'OFF' | 'AUTO'; /** * CurrentData Summary diff --git a/ui/src/app/shared/service/utils.ts b/ui/src/app/shared/service/utils.ts index 837e37acc54..63790ba3f8c 100644 --- a/ui/src/app/shared/service/utils.ts +++ b/ui/src/app/shared/service/utils.ts @@ -289,6 +289,17 @@ export class Utils { return new Date(value * 1000).toLocaleTimeString(); }; + /** + * Converts a date to Date and Time string. + * + * @param value the value from passed value in html + * @returns converted value + */ + public static CONVERT_DATE = (value: Date | number | string): string => { + const date = new Date(value); + return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); + }; + /** * Adds unit percentage [%] to a value. * @@ -337,6 +348,26 @@ export class Utils { }; }; + /** + * Converts states 'MANUAL', 'AUTO' and 'OFF' to translated strings. + * + * @param value the value from passed value in html + * @returns converted value + */ + public static CONVERT_MANUAL_AUTO_OFF = (translate: TranslateService) => { + return (value: DefaultTypes.ManualOffAuto): string => { + if (value === 'MANUAL') { + return translate.instant('General.manually'); + } else if (value === 'OFF') { + return translate.instant('General.off'); + } else if (value === 'AUTO') { + return translate.instant('General.auto'); + } else { + return '-'; + } + }; + }; + /** * Takes a power value and extracts the information if it represents Charge or Discharge. * diff --git a/ui/src/app/shared/type/widget.ts b/ui/src/app/shared/type/widget.ts index a6f73311e93..b0aea5fb3ae 100644 --- a/ui/src/app/shared/type/widget.ts +++ b/ui/src/app/shared/type/widget.ts @@ -22,23 +22,24 @@ export enum WidgetNature { } export enum WidgetFactory { - "Controller.Api.ModbusTcp.ReadWrite", - "Controller.Asymmetric.PeakShaving", - "Controller.ChannelThreshold", - "Controller.CHP.SoC", - "Controller.Ess.DelayedSellToGrid", - "Controller.Ess.FixActivePower", - "Controller.Ess.GridOptimizedCharge", - "Controller.Ess.Time-Of-Use-Tariff.Discharge", - "Controller.Ess.Time-Of-Use-Tariff", - "Controller.IO.ChannelSingleThreshold", - "Controller.Io.FixDigitalOutput", - "Controller.IO.HeatingElement", - "Controller.Io.HeatPump.SgReady", - "Controller.Symmetric.PeakShaving", - "Controller.TimeslotPeakshaving", - "Evcs.Cluster.PeakShaving", - "Evcs.Cluster.SelfConsumption", + "Controller.Api.ModbusTcp.ReadWrite", + 'Controller.Asymmetric.PeakShaving', + 'Controller.ChannelThreshold', + 'Controller.CHP.SoC', + 'Controller.Ess.DelayedSellToGrid', + 'Controller.Ess.Timeframe', + 'Controller.Ess.FixActivePower', + 'Controller.Ess.GridOptimizedCharge', + 'Controller.Ess.Time-Of-Use-Tariff.Discharge', + 'Controller.Ess.Time-Of-Use-Tariff', + 'Controller.IO.ChannelSingleThreshold', + 'Controller.Io.FixDigitalOutput', + 'Controller.IO.HeatingElement', + 'Controller.Io.HeatPump.SgReady', + 'Controller.Symmetric.PeakShaving', + 'Controller.TimeslotPeakshaving', + 'Evcs.Cluster.PeakShaving', + 'Evcs.Cluster.SelfConsumption', } export type Icon = { diff --git a/ui/src/assets/i18n/cz.json b/ui/src/assets/i18n/cz.json index 04ac9dd63ec..5f2b4c6edcd 100644 --- a/ui/src/assets/i18n/cz.json +++ b/ui/src/assets/i18n/cz.json @@ -5,6 +5,8 @@ "apply": "Aplikovat", "autarchy": "Soběstačnost", "automatic": "Automaticky", + "auto": "Auto", + "onDateAtTime": "dne {{date}} v {{time}}", "cancel": "zrušení", "capacity": "Kapacita", "changeAccepted": "Změna byla přijata", @@ -217,6 +219,11 @@ "continuousSellToGridPower": "Výboj níže", "relationError": "Limit poplatku musí být vyšší než limit vybití" }, + "Timeframe": { + "targetSoc": "Cílový stav nabití", + "endTime": "Čas ukončení", + "startTime": "Čas začátku" + }, "Peakshaving": { "asymmetricInfo": "Zadané hodnoty výkonu se vztahují k jednotlivým fázím. Je nastavena na nejvíce namáhanou fázi.", "endDate": "Datum Ukončení", diff --git a/ui/src/assets/i18n/de.json b/ui/src/assets/i18n/de.json index 954c76a43ce..c65bf47bb28 100644 --- a/ui/src/assets/i18n/de.json +++ b/ui/src/assets/i18n/de.json @@ -61,6 +61,11 @@ "continuousSellToGridPower": "Entladung unter", "relationError": "Beladungsgrenze muss größer der Entladungsgrenze sein" }, + "Timeframe": { + "targetSoc": "Ziel-Ladezustand", + "endTime": "End-Zeit", + "startTime": "Start-Zeit" + }, "Peakshaving": { "asymmetricInfo": "Eingetragene Leistungswerte beziehen sich auf einzelne Phasen. Es wird auf die jeweils am stärksten belastete Phase ausgeregelt.", "endDate": "End Datum", @@ -466,6 +471,7 @@ "apply": "Übernehmen", "autarchy": "Autarkie", "automatic": "Automatisch", + "auto": "Auto", "cancel": "abbrechen", "capacity": "Kapazität", "changeAccepted": "Änderung übernommen", @@ -508,6 +514,7 @@ "offGrid": "Keine Netzverbindung!", "ok": "ok", "on": "An", + "onDateAtTime": "{{date}} um {{time}}", "otherConsumption": "Sonstiger", "percentage": "Prozent", "periodFromTo": "{{ value1 }} - {{ value2 }}", diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index 0ce4b7372fb..c46df18121b 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -61,6 +61,11 @@ "continuousSellToGridPower": "Discharge below", "relationError": "Charge limit must be greater than the Discharge limit" }, + "Timeframe": { + "targetSoc": "Target state of charge", + "endTime": "End Time", + "startTime": "Start Time" + }, "Peakshaving": { "asymmetricInfo": "The entered performance values ​​refer to individual phases. It is adjusted to the most stressed phase.", "endDate": "End date", @@ -467,6 +472,7 @@ "apply": "Apply", "autarchy": "Autarchy", "automatic": "Automatically", + "auto": "Auto", "cancel": "cancel", "capacity": "Capacity", "changeAccepted": "Change accepted", @@ -563,6 +569,7 @@ "netherlands": "Netherlands", "switzerland": "Switzerland" }, + "onDateAtTime": "on {{date}} at {{time}}", "yes": "Yes", "no": "No", "value": "value", diff --git a/ui/src/assets/i18n/es.json b/ui/src/assets/i18n/es.json index 251dd41029d..a7d64ef2155 100644 --- a/ui/src/assets/i18n/es.json +++ b/ui/src/assets/i18n/es.json @@ -4,6 +4,8 @@ "actualPower": "e-car Carga", "autarchy": "Autosuficiencia", "automatic": "Automático", + "auto": "Auto", + "onDateAtTime": "en {{date}} a las {{time}}", "cancel": "cancelar", "capacity": "Capacidad", "changeAccepted": "Cambio aceptado", @@ -207,6 +209,11 @@ "continuousSellToGridPower": "Descarga a continuación", "relationError": "El límite de carga debe ser mayor que el límite de descarga" }, + "Timeframe": { + "targetSoc": "Estado de carga objetivo", + "endTime": "Hora de finalización", + "startTime": "Hora de inicio" + }, "Peakshaving": { "asymmetricInfo": "Los valores de rendimiento introducidos se refieren a fases individuales. Se ajusta a la fase más estresada.", "endDate": "Fecha final", diff --git a/ui/src/assets/i18n/fr.json b/ui/src/assets/i18n/fr.json index 2ead4d8e8c0..2d76785ed30 100644 --- a/ui/src/assets/i18n/fr.json +++ b/ui/src/assets/i18n/fr.json @@ -5,6 +5,8 @@ "apply": "Apply", "autarchy": "Autarcie", "automatic": "Automatiquement", + "auto": "Auto", + "onDateAtTime": "le {{date}} à {{time}}", "cancel": "cancel", "capacity": "Capacité", "numberOfComponents": "nombre de composants", @@ -214,6 +216,11 @@ "continuousSellToGridPower": "Décharge ci-dessous", "relationError": "La limite de charge doit être supérieure à la limite de décharge" }, + "Timeframe": { + "targetSoc": "État de charge cible", + "endTime": "Heure de fin", + "startTime": "Heure de début" + }, "Peakshaving": { "asymmetricInfo": "The entered performance values ​​refer to individual phases. It is adjusted to the most stressed phase.", "endDate": "Date de fin", diff --git a/ui/src/assets/i18n/ja.json b/ui/src/assets/i18n/ja.json index 06a7294eb4f..281dc7b9645 100644 --- a/ui/src/assets/i18n/ja.json +++ b/ui/src/assets/i18n/ja.json @@ -61,6 +61,11 @@ "continuousSellToGridPower": "以下で放電します", "relationError": "充電制限は、排出制限よりも大きくなければなりません" }, + "Timeframe": { + "targetSoc": "目標の充電状態", + "endTime": "終了時間", + "startTime": "開始時間" + }, "Peakshaving": { "asymmetricInfo": "入力されたパフォーマンス値は、個々のフェーズを指します。最も強調されたフェーズに調整されます。", "endDate": "終了日", @@ -405,6 +410,8 @@ "apply": "適用", "autarchy": "再生可能エネルギー率", "automatic": "自動", + "auto": "自動", + "onDateAtTime": "{{date}}の{{time}}に", "cancel": "キャンセル", "capacity": "容量", "changeAccepted": "変更が承認されました", diff --git a/ui/src/assets/i18n/nl.json b/ui/src/assets/i18n/nl.json index 77d013c0482..a750798c06e 100644 --- a/ui/src/assets/i18n/nl.json +++ b/ui/src/assets/i18n/nl.json @@ -4,6 +4,8 @@ "actualPower": "e-car Laad vermogen", "autarchy": "Autarkie", "automatic": "Automatisch", + "auto": "Auto", + "onDateAtTime": "op {{date}} om {{time}}", "cancel": "annuleren", "capacity": "Hoedanigheid", "changeAccepted": "Wijziging geaccepteerd", @@ -197,6 +199,11 @@ "switchOnBelow": "Inschakelen onder", "threshold": "Thresholded" }, + "Timeframe": { + "targetSoc": "Doel laadstatus", + "endTime": "Eindtijd", + "startTime": "Starttijd" + }, "Peakshaving": { "asymmetricInfo": "De ingevoerde prestatiewaarden verwijzen naar afzonderlijke fasen. Het is aangepast aan de meest gestresste fase.", "endDate": "Einddatum",