logs = new ArrayList<>();
+ private boolean debugMode = false;
+
+ public UpdateState() {
+ this.reset();
+ }
+
+ public void setRunning(boolean isRunning) {
+ this.isRunning.set(isRunning);
+ }
+
+ public boolean isRunning() {
+ return this.isRunning.get();
+ }
+
+ public void setPercentCompleted(int percentCompleted) {
+ this.percentCompleted.set(percentCompleted);
+ }
+
+ public synchronized void setDebugMode(boolean debugMode) {
+ this.debugMode = debugMode;
+ }
+
+ /**
+ * Adds a line to the log.
+ *
+ * @param line the line
+ */
+ public void addLog(String line) {
+ synchronized (this.log) {
+ this.log.info("System-Update: " + line);
+ if (this.debugMode) {
+ this.logs.add(line);
+ }
+ }
+ }
+
+ /**
+ * Adds a {@link Exception} to the log.
+ *
+ * @param e the {@link Exception}
+ */
+ public void addLog(Exception e) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ e.printStackTrace(pw);
+ this.addLog(pw.toString());
+ }
+
+ /**
+ * Adds a {@link ExecuteSystemCommandResponse} with a label to the log.
+ *
+ * @param label the label
+ * @param response the {@link ExecuteSystemCommandResponse}
+ */
+ public void addLog(String label, ExecuteSystemCommandResponse response) {
+ synchronized (this.log) {
+ String[] stdout = response.getStdout();
+ if (stdout.length > 0) {
+ this.addLog(label + ": STDOUT");
+ for (String line : stdout) {
+ this.addLog(label + ": " + line);
+ }
+ }
+ String[] stderr = response.getStderr();
+ if (stderr.length > 0) {
+ this.addLog(label + ": STDERR");
+ for (String line : stderr) {
+ this.addLog(label + ": " + line);
+ }
+ }
+ if (response.getExitCode() == 0) {
+ this.addLog(label + ": FINISHED SUCCESSFULLY");
+ } else {
+ this.addLog(label + ": FINISHED WITH ERROR CODE [" + response.getExitCode() + "]");
+ }
+ }
+ }
+
+ protected JsonObject toJsonObject() {
+ JsonArrayBuilder logs = JsonUtils.buildJsonArray();
+ synchronized (this.log) {
+ for (String log : this.logs) {
+ logs.add(log);
+ }
+ }
+ return JsonUtils.buildJsonObject() //
+ .add("running", JsonUtils.buildJsonObject() //
+ .addProperty("percentCompleted", this.percentCompleted.get()) //
+ .add("logs", logs.build()) //
+ .build()) //
+ .build();
+ }
+
+ /**
+ * Resets the {@link UpdateState} object.
+ */
+ public synchronized void reset() {
+ this.isRunning.set(false);
+ this.percentCompleted.set(0);
+ synchronized (this.log) {
+ this.logs.clear();
+ }
+ }
+ }
+
+ private static class Running implements SystemUpdateState {
+ private final UpdateState updateState;
+
+ public Running(UpdateState updateState) {
+ this.updateState = updateState;
+ }
+
+ @Override
+ public JsonObject toJsonObject() {
+ return this.updateState.toJsonObject();
+ }
+ }
+
+ private final SystemUpdateState state;
+
+ /**
+ * Builds a {@link GetSystemUpdateStateResponse} for {@link Running} state.
+ *
+ * @param id the request ID
+ * @param updateState the {@link UpdateState}
+ * @return the {@link GetSystemUpdateStateResponse}
+ */
+ public static GetSystemUpdateStateResponse isRunning(UUID id, UpdateState updateState) {
+ return new GetSystemUpdateStateResponse(id, new Running(updateState));
+ }
+
+ /**
+ * Builds a {@link GetSystemUpdateStateResponse} for {@link Unknown},
+ * {@link Updated} or {@link Available} state.
+ *
+ * @param id the request ID
+ * @param currentVersion the current version
+ * @param latestVersion the latest version
+ * @return the {@link GetSystemUpdateStateResponse}
+ */
+ public static GetSystemUpdateStateResponse from(UUID id, String currentVersion, String latestVersion) {
+ final SemanticVersion current;
+ final SemanticVersion latest;
+ try {
+ current = SemanticVersion.fromString(currentVersion);
+ latest = SemanticVersion.fromString(latestVersion);
+ } catch (NumberFormatException e) {
+ return new GetSystemUpdateStateResponse(id, new Unknown());
+ }
+ if (current.isAtLeast(latest)) {
+ return new GetSystemUpdateStateResponse(id, new Updated(current));
+ } else {
+ return new GetSystemUpdateStateResponse(id, new Available(current, latest));
+ }
+ }
+
+ private GetSystemUpdateStateResponse(UUID id, SystemUpdateState state) {
+ super(id);
+ this.state = state;
+ }
+
+ @Override
+ public JsonObject getResult() {
+ return this.state.toJsonObject();
+ }
+
+}
diff --git a/io.openems.edge.core/src/io/openems/edge/core/host/jsonrpc/SystemUpdateRequest.java b/io.openems.edge.core/src/io/openems/edge/core/host/jsonrpc/SystemUpdateRequest.java
new file mode 100644
index 00000000000..8e8d76ba775
--- /dev/null
+++ b/io.openems.edge.core/src/io/openems/edge/core/host/jsonrpc/SystemUpdateRequest.java
@@ -0,0 +1,45 @@
+package io.openems.edge.core.host.jsonrpc;
+
+import com.google.gson.JsonObject;
+
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.common.exceptions.OpenemsException;
+import io.openems.common.jsonrpc.base.JsonrpcRequest;
+
+/**
+ * Represents a JSON-RPC Request to execute a system update.
+ *
+ *
+ * {
+ * "jsonrpc": "2.0",
+ * "id": "UUID",
+ * "method": "systemUpdate",
+ * "params": {}
+ * }
+ *
+ */
+public class SystemUpdateRequest extends JsonrpcRequest {
+
+ public static final String METHOD = "systemUpdate";
+
+ /**
+ * Parses a generic {@link JsonrpcRequest} to a {@link SystemUpdateRequest}.
+ *
+ * @param r the {@link JsonrpcRequest}
+ * @return the {@link SystemUpdateRequest}
+ * @throws OpenemsNamedException on error
+ */
+ public static SystemUpdateRequest from(JsonrpcRequest r) throws OpenemsException {
+ return new SystemUpdateRequest(r);
+ }
+
+ private SystemUpdateRequest(JsonrpcRequest request) {
+ super(request, METHOD);
+ }
+
+ @Override
+ public JsonObject getParams() {
+ return new JsonObject();
+ }
+
+}
diff --git a/io.openems.edge.core/test/io/openems/edge/core/host/HostImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/host/HostImplTest.java
new file mode 100644
index 00000000000..1af427c587b
--- /dev/null
+++ b/io.openems.edge.core/test/io/openems/edge/core/host/HostImplTest.java
@@ -0,0 +1,56 @@
+package io.openems.edge.core.host;
+
+/**
+ * This is not a real JUnit test, but can be used to mock the JsonRpc-Calls from
+ * UI.
+ */
+public class HostImplTest {
+
+// private final static User OWNER = new DummyUser("owner", "owner", Role.OWNER);
+//
+// @Test
+// public void test() throws OpenemsException, Exception {
+// final DummyConfigurationAdmin cm = new DummyConfigurationAdmin();
+// cm.getOrCreateEmptyConfiguration(Host.SINGLETON_SERVICE_PID);
+// final HostImpl sut = new HostImpl();
+//
+// new ComponentTest(sut) //
+// .addReference("cm", cm) //
+// .activate(MyConfig.create() //
+// .setNetworkConfiguration("") //
+// .setUsbConfiguration("") //
+// .build());
+//
+// {
+// CompletableFuture extends JsonrpcResponseSuccess> future = sut.handleJsonrpcRequest(OWNER,
+// new GetSystemUpdateStateRequest());
+// Thread.sleep(1000);
+// JsonrpcResponseSuccess response = future.get();
+// System.out.println(response.getResult());
+// }
+// {
+// CompletableFuture extends JsonrpcResponseSuccess> future = sut.handleJsonrpcRequest(OWNER,
+// new ExecuteSystemUpdateRequest(true));
+// for (int i = 0; i < 2; i++) {
+// Thread.sleep(500);
+// CompletableFuture extends JsonrpcResponseSuccess> future2 = sut.handleJsonrpcRequest(OWNER,
+// new GetSystemUpdateStateRequest());
+// JsonrpcResponseSuccess response = future2.get();
+// System.out.println(response.getResult());
+// }
+//
+// JsonrpcResponseSuccess response = future.get();
+// System.out.println("FINISHED");
+// JsonUtils.prettyPrint(response.getResult());
+// }
+//
+// Thread.sleep(2000);
+// CompletableFuture extends JsonrpcResponseSuccess> future2 = sut.handleJsonrpcRequest(OWNER,
+// new GetSystemUpdateStateRequest());
+// JsonrpcResponseSuccess response = future2.get();
+// System.out.println(response.getResult());
+//
+// Thread.sleep(10000);
+// }
+
+}
diff --git a/io.openems.edge.core/test/io/openems/edge/core/host/MyConfig.java b/io.openems.edge.core/test/io/openems/edge/core/host/MyConfig.java
new file mode 100644
index 00000000000..3c20a88b8ab
--- /dev/null
+++ b/io.openems.edge.core/test/io/openems/edge/core/host/MyConfig.java
@@ -0,0 +1,57 @@
+package io.openems.edge.core.host;
+
+import io.openems.edge.common.host.Host;
+import io.openems.edge.common.test.AbstractComponentConfig;
+
+@SuppressWarnings("all")
+public class MyConfig extends AbstractComponentConfig implements Config {
+
+ protected static class Builder {
+ public String networkConfiguration = null;
+ public String usbConfiguration = null;
+
+ private Builder() {
+ }
+
+ public Builder setNetworkConfiguration(String networkConfiguration) {
+ this.networkConfiguration = networkConfiguration;
+ return this;
+ }
+
+ public Builder setUsbConfiguration(String usbConfiguration) {
+ this.usbConfiguration = usbConfiguration;
+ 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, Host.SINGLETON_COMPONENT_ID);
+ this.builder = builder;
+ }
+
+ @Override
+ public String networkConfiguration() {
+ return this.builder.networkConfiguration;
+ }
+
+ @Override
+ public String usbConfiguration() {
+ return this.builder.usbConfiguration;
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java
index d3d4678aaaf..98925e7c334 100644
--- a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java
+++ b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java
@@ -28,6 +28,7 @@
import io.openems.edge.common.event.EdgeEventConstants;
import io.openems.edge.common.sum.Sum;
import io.openems.edge.ess.api.ManagedSymmetricEss;
+import io.openems.edge.ess.api.SymmetricEss;
import io.openems.edge.evcs.api.Evcs;
import io.openems.edge.evcs.api.ManagedEvcs;
import io.openems.edge.meter.api.AsymmetricMeter;
@@ -66,7 +67,7 @@ public class EvcsClusterPeakShaving extends AbstractEvcsCluster implements Opene
protected Sum sum;
@Reference
- private ManagedSymmetricEss ess;
+ private SymmetricEss ess;
@Reference
private SymmetricMeter meter;
@@ -182,12 +183,18 @@ public int getMaximumPowerToDistribute() {
int maxEssDischarge = 0;
long maxAvailableStoragePower = 0;
- maxEssDischarge = this.ess.getAllowedDischargePower().orElse(0);
+ if(this.ess instanceof ManagedSymmetricEss) {
+ maxEssDischarge = ((ManagedSymmetricEss)this.ess).getAllowedDischargePower().orElse(0);
+ // TODO: Use PowerComponent
+ } else {
+ maxEssDischarge = this.ess.getMaxApparentPower().orElse(0);
+ }
+
if (this.config.enable_secure_ess_discharge()) {
maxEssDischarge = this.getSecureEssDischargePower(maxEssDischarge);
this.channel(AbstractEvcsCluster.ChannelId.USED_ESS_MAXIMUM_DISCHARGE_POWER).setNextValue(maxEssDischarge);
}
- // TODO: Should I use power component here
+
// TODO: Calculate the available ESS charge power, depending on a specific ESS
// component (e.g. If there is a ESS cluster)
diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/Config.java b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/Config.java
index 0e44b13f3a3..32e35968062 100644
--- a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/Config.java
+++ b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/Config.java
@@ -23,12 +23,12 @@
@AttributeDefinition(name = "IP-Address", description = "The IP address of the charging station. If the charger has two connectors, the second/slave evcs has the IP 192.168.25.31.", required = true)
String ip() default "192.168.25.30";
- @AttributeDefinition(name = "Minimum power", description = "Minimum current of the Charger in mA.", required = true)
+ @AttributeDefinition(name = "Minimum hardware current", description = "Minimum current of the Charger in mA.", required = true)
int minHwCurrent() default 6000;
- @AttributeDefinition(name = "Maximum power", description = "Maximum current of the Charger in mA.", required = true)
+ @AttributeDefinition(name = "Maximum hardware current", description = "Maximum current of the Charger in mA.", required = true)
int maxHwCurrent() default 32000;
-
+
String webconsole_configurationFactory_nameHint() default "EVCS Hardy Barth [{id}]";
}
\ No newline at end of file
diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarth.java b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarth.java
index a75a28975a3..a3dd6349081 100644
--- a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarth.java
+++ b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarth.java
@@ -7,8 +7,11 @@
import io.openems.common.types.OpenemsType;
import io.openems.edge.common.channel.BooleanReadChannel;
import io.openems.edge.common.channel.Doc;
+import io.openems.edge.common.type.TypeUtils;
-interface HardyBarth {
+public interface HardyBarth {
+
+ public static final double SCALE_FACTOR_MINUS_1 = 0.1;
public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
@@ -40,6 +43,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
// SALIA
RAW_SALIA_CHARGE_MODE(Doc.of(OpenemsType.STRING), "secc", "port0", "salia", "chargemode"), //
+ RAW_SALIA_CHANGE_METER(Doc.of(OpenemsType.STRING), "secc", "port0", "salia", "changemeter"), //
RAW_SALIA_AUTHMODE(Doc.of(OpenemsType.STRING), "secc", "port0", "salia", "authmode"), //
RAW_SALIA_FIRMWARESTATE(Doc.of(OpenemsType.STRING), "secc", "port0", "salia", "firmwarestate"), //
RAW_SALIA_FIRMWAREPROGRESS(Doc.of(OpenemsType.STRING), "secc", "port0", "salia", "firmwareprogress"), //
@@ -73,23 +77,34 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
}), "secc", "port0", "metering", "meter", "available"), //
// METERING - POWER
- RAW_ACTIVE_POWER_L1(Doc.of(OpenemsType.LONG), "secc", "port0", "metering", "power", "active", "ac", "l1",
- "actual"), //
- RAW_ACTIVE_POWER_L2(Doc.of(OpenemsType.LONG), "secc", "port0", "metering", "power", "active", "ac", "l2",
- "actual"), //
- RAW_ACTIVE_POWER_L3(Doc.of(OpenemsType.LONG), "secc", "port0", "metering", "power", "active", "ac", "l2",
- "actual"), //
+ RAW_ACTIVE_POWER_L1(Doc.of(OpenemsType.LONG).unit(Unit.WATT), (value) -> {
+ Double doubleValue = TypeUtils.getAsType(OpenemsType.DOUBLE, value);
+ return TypeUtils.getAsType(OpenemsType.LONG, TypeUtils.multiply(doubleValue, SCALE_FACTOR_MINUS_1));
+ }, "secc", "port0", "metering", "power", "active", "ac", "l1", "actual"), //
+
+ RAW_ACTIVE_POWER_L2(Doc.of(OpenemsType.LONG).unit(Unit.WATT), (value) -> {
+ Double doubleValue = TypeUtils.getAsType(OpenemsType.DOUBLE, value);
+ return TypeUtils.getAsType(OpenemsType.LONG, TypeUtils.multiply(doubleValue, SCALE_FACTOR_MINUS_1));
+ }, "secc", "port0", "metering", "power", "active", "ac", "l2", "actual"), //
+
+ RAW_ACTIVE_POWER_L3(Doc.of(OpenemsType.LONG).unit(Unit.WATT), (value) -> {
+ Double doubleValue = TypeUtils.getAsType(OpenemsType.DOUBLE, value);
+ return TypeUtils.getAsType(OpenemsType.LONG, TypeUtils.multiply(doubleValue, SCALE_FACTOR_MINUS_1));
+ }, "secc", "port0", "metering", "power", "active", "ac", "l2", "actual"), //
// METERING - CURRENT
- RAW_ACTIVE_CURRENT_L1(Doc.of(OpenemsType.STRING), "secc", "port0", "metering", "current", "ac", "l1", "actual"), //
- RAW_ACTIVE_CURRENT_L2(Doc.of(OpenemsType.STRING), "secc", "port0", "metering", "current", "ac", "l2", "actual"), //
- RAW_ACTIVE_CURRENT_L3(Doc.of(OpenemsType.STRING), "secc", "port0", "metering", "current", "ac", "l3", "actual"), //
+ RAW_ACTIVE_CURRENT_L1(Doc.of(OpenemsType.LONG).unit(Unit.MILLIAMPERE), "secc", "port0", "metering", "current",
+ "ac", "l1", "actual"), //
+ RAW_ACTIVE_CURRENT_L2(Doc.of(OpenemsType.LONG).unit(Unit.MILLIAMPERE), "secc", "port0", "metering", "current",
+ "ac", "l2", "actual"), //
+ RAW_ACTIVE_CURRENT_L3(Doc.of(OpenemsType.LONG).unit(Unit.MILLIAMPERE), "secc", "port0", "metering", "current",
+ "ac", "l3", "actual"), //
// METERING - ENERGY
- RAW_ACTIVE_ENERGY_TOTAL(Doc.of(OpenemsType.DOUBLE), "secc", "port0", "metering", "energy", "active_total",
- "actual"), //
- RAW_ACTIVE_ENERGY_EXPORT(Doc.of(OpenemsType.DOUBLE), "secc", "port0", "metering", "energy", "active_export",
- "actual"), //
+ RAW_ACTIVE_ENERGY_TOTAL(Doc.of(OpenemsType.DOUBLE).unit(Unit.WATT_HOURS), "secc", "port0", "metering", "energy",
+ "active_total", "actual"), //
+ RAW_ACTIVE_ENERGY_EXPORT(Doc.of(OpenemsType.DOUBLE).unit(Unit.WATT_HOURS), "secc", "port0", "metering",
+ "energy", "active_export", "actual"), //
// EMERGENCY SHUTDOWN
RAW_EMERGENCY_SHUTDOWN(Doc.of(OpenemsType.STRING), "secc", "port0", "emergency_shutdown"), //
diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadWorker.java b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadWorker.java
index a59d95fe793..fe4f9a9015e 100644
--- a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadWorker.java
+++ b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthReadWorker.java
@@ -9,12 +9,14 @@
import io.openems.common.utils.JsonUtils;
import io.openems.common.worker.AbstractCycleWorker;
import io.openems.edge.common.channel.ChannelId;
+import io.openems.edge.common.type.TypeUtils;
import io.openems.edge.evcs.api.Evcs;
import io.openems.edge.evcs.api.Status;
public class HardyBarthReadWorker extends AbstractCycleWorker {
private final HardyBarthImpl parent;
+ private int chargingFinishedCounter = 0;
public HardyBarthReadWorker(HardyBarthImpl parent) {
this.parent = parent;
@@ -59,15 +61,6 @@ protected void forever() throws OpenemsNamedException {
*/
private void setEvcsChannelIds(JsonElement json) {
- // CHARGE_POWER
- Long chargePower = (Long) this.getValueFromJson(Evcs.ChannelId.CHARGE_POWER, json, (value) -> {
- if (value == null) {
- return null;
- }
- return Math.round((Integer) value * 0.1);
- }, "secc", "port0", "metering", "power", "active_total", "actual");
- this.parent._setChargePower(chargePower == null ? null : chargePower.intValue());
-
// ENERGY_SESSION
Double energy = (Double) this.getValueFromJson(Evcs.ChannelId.ENERGY_SESSION, OpenemsType.STRING, json,
(value) -> {
@@ -77,7 +70,8 @@ private void setEvcsChannelIds(JsonElement json) {
Double rawEnergy = null;
String[] chargedata = value.toString().split("\\|");
if (chargedata.length == 3) {
- rawEnergy = Double.parseDouble(chargedata[2]) * 1000;
+ Double doubleValue = TypeUtils.getAsType(OpenemsType.DOUBLE, chargedata[2]);
+ rawEnergy = doubleValue * 1000;
}
return rawEnergy;
@@ -86,35 +80,33 @@ private void setEvcsChannelIds(JsonElement json) {
// ACTIVE_CONSUMPTION_ENERGY
Long activeConsumptionEnergy = (Long) this.getValueFromJson(Evcs.ChannelId.ACTIVE_CONSUMPTION_ENERGY, json,
- (value) -> (long) (Double.parseDouble(value.toString()) * 0.1), "secc", "port0", "metering", "energy",
- "active_import", "actual"); //
+ (value) -> {
+ Double doubleValue = TypeUtils.getAsType(OpenemsType.DOUBLE, value);
+ return TypeUtils.getAsType(OpenemsType.LONG, doubleValue);
+
+ }, "secc", "port0", "metering", "energy", "active_import", "actual"); //
this.parent._setActiveConsumptionEnergy(activeConsumptionEnergy);
// PHASES
- Double powerL1 = (Double) this.getValueFromJson(HardyBarth.ChannelId.RAW_ACTIVE_POWER_L1, json,
- (value) -> Double.parseDouble(value.toString()) * 0.1, "secc", "port0", "metering", "power", "active",
- "ac", "l1", "actual");
- Double powerL2 = (Double) this.getValueFromJson(HardyBarth.ChannelId.RAW_ACTIVE_POWER_L2, json,
- (value) -> Double.parseDouble(value.toString()) * 0.1, "secc", "port0", "metering", "power", "active",
- "ac", "l2", "actual");
- Double powerL3 = (Double) this.getValueFromJson(HardyBarth.ChannelId.RAW_ACTIVE_POWER_L3, json,
- (value) -> Double.parseDouble(value.toString()) * 0.1, "secc", "port0", "metering", "power", "active",
- "ac", "l3", "actual");
+ Long powerL1 = (Long) this.getValueForChannel(HardyBarth.ChannelId.RAW_ACTIVE_POWER_L1, json);
+ Long powerL2 = (Long) this.getValueForChannel(HardyBarth.ChannelId.RAW_ACTIVE_POWER_L2, json);
+ Long powerL3 = (Long) this.getValueForChannel(HardyBarth.ChannelId.RAW_ACTIVE_POWER_L3, json);
+
Integer phases = null;
if (powerL1 != null && powerL2 != null && powerL3 != null) {
- Double sum = powerL1 + powerL2 + powerL3;
+ Long sum = powerL1 + powerL2 + powerL3;
- if (sum > 300) {
+ if (sum > 900) {
phases = 0;
- if (powerL1 >= 100) {
+ if (powerL1 >= 300) {
phases += 1;
}
- if (powerL2 >= 100) {
+ if (powerL2 >= 300) {
phases += 1;
}
- if (powerL3 >= 100) {
+ if (powerL3 >= 300) {
phases += 1;
}
}
@@ -124,24 +116,61 @@ private void setEvcsChannelIds(JsonElement json) {
this.parent.debugLog("Used phases: " + phases);
}
+ this.parent._setMinimumHardwarePower(this.parent.config.minHwCurrent() / 1000 * 3 * 230);
+ this.parent._setMaximumHardwarePower(this.parent.config.maxHwCurrent() / 1000 * 3 * 230);
+
+ // CHARGE_POWER
+ Long chargePowerLong = (Long) this.getValueFromJson(Evcs.ChannelId.CHARGE_POWER, json, (value) -> {
+ Integer integerValue = TypeUtils.getAsType(OpenemsType.INTEGER, value);
+ if (integerValue == null) {
+ return null;
+ }
+
+ long activePower = Math.round(integerValue * HardyBarth.SCALE_FACTOR_MINUS_1);
+
+ // Ignore the consumption of the charger itself
+ return activePower < 100 ? 0 : activePower;
+ }, "secc", "port0", "metering", "power", "active_total", "actual");
+
+ //
+ this.parent._setChargePower(chargePowerLong == null ? null : chargePowerLong.intValue());
+
// STATUS
Status status = (Status) this.getValueFromJson(HardyBarth.ChannelId.RAW_CHARGE_STATUS_CHARGEPOINT, json,
(value) -> {
+ String stringValue = TypeUtils.getAsType(OpenemsType.STRING, value);
+ if (stringValue == null) {
+ return Status.UNDEFINED;
+ }
+
Status rawStatus = Status.UNDEFINED;
- switch (value.toString()) {
+ switch (stringValue) {
case "A":
rawStatus = Status.NOT_READY_FOR_CHARGING;
break;
case "B":
- rawStatus = Status.CHARGING_REJECTED;
- if (this.parent.getSetChargePowerLimit().orElse(0) > this.parent.getMinimumHardwarePower()
- .orElse(0)) {
- rawStatus = Status.CHARGING_FINISHED;
+ rawStatus = Status.READY_FOR_CHARGING;
+
+ // Detect if the car is full
+ int chargePower = chargePowerLong == null ? 0 : chargePowerLong.intValue();
+ if (this.parent.getSetChargePowerLimit().orElse(0) >= this.parent.getMinimumHardwarePower()
+ .orElse(0) && chargePower <= 0) {
+
+ if (this.chargingFinishedCounter >= 90) {
+ rawStatus = Status.CHARGING_FINISHED;
+ } else {
+ this.chargingFinishedCounter++;
+ }
+ } else {
+ this.chargingFinishedCounter = 0;
+
+ // Charging rejected because we are forcing to pause charging
+ if (this.parent.getSetChargePowerLimit().orElse(0) == 0) {
+ rawStatus = Status.CHARGING_REJECTED;
+ }
}
break;
case "C":
- rawStatus = Status.CHARGING;
- break;
case "D":
rawStatus = Status.CHARGING;
break;
@@ -153,12 +182,28 @@ private void setEvcsChannelIds(JsonElement json) {
rawStatus = Status.UNDEFINED;
break;
}
+ if (stringValue.equals("B")) {
+ this.chargingFinishedCounter = 0;
+ }
return rawStatus;
}, "secc", "port0", "ci", "charge", "cp", "status");
this.parent._setStatus(status);
}
+ /**
+ * Call the getValueFromJson with the detailed information of the channel.
+ *
+ * @param channelId Channel that value will be detect.
+ * @param json Whole JSON path, where the JsonElement for the given channel
+ * is located.
+ * @return Value of the last JsonElement by running through the specified JSON
+ * path.
+ */
+ private Object getValueForChannel(HardyBarth.ChannelId channelId, JsonElement json) {
+ return this.getValueFromJson(channelId, json, channelId.converter, channelId.getJsonPaths());
+ }
+
/**
* Call the getValueFromJson without a divergent type in the raw json.
*
diff --git a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthWriteHandler.java b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthWriteHandler.java
index 13219634a01..1099e364bab 100644
--- a/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthWriteHandler.java
+++ b/io.openems.edge.evcs.hardybarth/src/io/openems/edge/evcs/hardybarth/HardyBarthWriteHandler.java
@@ -24,7 +24,7 @@ public class HardyBarthWriteHandler implements Runnable {
/*
* Minimum pause between two consecutive writes.
*/
- private static final int WRITE_INTERVAL_SECONDS = 30;
+ private static final int WRITE_INTERVAL_SECONDS = 1;
public HardyBarthWriteHandler(HardyBarthImpl parent) {
this.parent = parent;
@@ -38,6 +38,8 @@ public void run() {
}
this.setManualMode();
+ this.setHeartbeat();
+ // this.enableExternalMeter();
this.setEnergyLimit();
this.setPower();
}
@@ -65,6 +67,60 @@ private void setManualMode() {
}
}
+ /**
+ * Set heartbeat.
+ *
+ *
+ * Sets the heartbeat to on or off.
+ */
+ private void setHeartbeat() {
+ // The internal heartbeat is currently too fast - it is not enough to write
+ // every second by default. We have to disable it to run the evcs
+ // properly.
+ // TODO: The manufacturer must be asked if it is possible to read the heartbeat
+ // status so that we can check if the heartbeat is really disabled and if the
+ // heartbeat time can be increased to be able to use this feature.
+
+ try {
+ this.parent.api.sendPutRequest("/api/secc", "salia/heartbeat", "off");
+ } catch (OpenemsNamedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Enable external meter.
+ *
+ *
+ * Enables the external meter if not set.
+ */
+ // TODO: Set the external meter to true because it's disabled per default.
+ // Not usable for now, because we haven't an update process defined and
+ // this REST Entry is only available with a beta firmware
+ // (http://salia.echarge.de/firmware/firmware_1.37.8_beta.image) or the next
+ // higher stable version. Be aware that the REST call and the update should not
+ // be called every cycle
+ /*
+ * private void enableExternalMeter() {
+ *
+ * BooleanReadChannel channelChargeMode =
+ * this.parent.channel(HardyBarth.ChannelId.RAW_SALIA_CHANGE_METER);
+ * Optional valueOpt = channelChargeMode.value().asOptional(); if
+ * (valueOpt.isPresent()) { if (!valueOpt.get().equals(true)) { // Enable
+ * external meter try {
+ * this.parent.debugLog("Enable external meter of HardyBarth " +
+ * this.parent.id()); JsonElement result =
+ * this.parent.api.sendPutRequest("/api/secc", "salia/changemeter",
+ * "enable | /dev/ttymxc0 | klefr | 9600 | none | 1");
+ * this.parent.debugLog(result.toString());
+ *
+ * if (result.toString().equals("{\"result\":\"ok\"}")) { // Reboot the charger
+ * this.parent.debugLog("Reboot of HardyBarth " + this.parent.id()); JsonElement
+ * resultReboot = this.parent.api.sendPutRequest("/api/secc",
+ * "salia/servicereboot", "1"); this.parent.debugLog(resultReboot.toString()); }
+ * } catch (OpenemsNamedException e) { e.printStackTrace(); } } } }
+ */
+
/**
* Sets the current from SET_CHARGE_POWER channel.
*
@@ -128,17 +184,28 @@ private void setPower() {
*/
private void setTarget(int current, int power) {
try {
+ JsonElement resultPause;
+ if (current > 0) {
+ // Send stop pause request
+ resultPause = this.parent.api.sendPutRequest("/api/secc", "salia/pausecharging", "" + 0);
+ this.parent.debugLog("Wake up HardyBarth " + this.parent.alias() + " from the pause");
+ } else {
+ // Send pause charging request
+ resultPause = this.parent.api.sendPutRequest("/api/secc", "salia/pausecharging", "" + 1);
+ this.parent.debugLog("Setting HardyBarth " + this.parent.alias() + " to pause");
+ }
+
// Send charge power limit
JsonElement result = this.parent.api.sendPutRequest("/api/secc", "grid_current_limit", "" + current);
// Set results
- this.parent._setSetChargePowerLimit(current);
- this.parent.debugLog(result.toString());
+ this.parent._setSetChargePowerLimit(power);
+ this.parent.debugLog("Pause: " + resultPause.toString());
+ this.parent.debugLog("SetActivePower: " + result.toString());
// Prepare next write
this.nextCurrentWrite = LocalDateTime.now().plusSeconds(WRITE_INTERVAL_SECONDS);
this.lastCurrent = current;
- this.parent._setSetChargePowerLimit(power);
} catch (OpenemsNamedException e) {
e.printStackTrace();
}
diff --git a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/KebaKeContact.java b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/KebaKeContact.java
index 0594c8592a7..456f26c3689 100644
--- a/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/KebaKeContact.java
+++ b/io.openems.edge.evcs.keba.kecontact/src/io/openems/edge/evcs/keba/kecontact/KebaKeContact.java
@@ -81,7 +81,7 @@ void activate(ComponentContext context, Config config) throws UnknownHostExcepti
this.channel(KebaChannelId.ALIAS).setNextValue(config.alias());
- this.ip = Inet4Address.getByName(config.ip());
+ this.ip = Inet4Address.getByName(config.ip().trim());
this.config = config;
this._setPowerPrecision(0.23);
diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java
index 462768b9996..fce0e05867f 100644
--- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java
+++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java
@@ -288,18 +288,20 @@ private void setBatteryLimits(Battery battery) throws OpenemsNamedException {
if ((bmsChargeMaxCurrent.isDefined() && !Objects.equals(bmsChargeMaxCurrent.get(), setChargeMaxCurrent))
|| (bmsDischargeMaxCurrent.isDefined()
&& !Objects.equals(bmsDischargeMaxCurrent.get(), setDischargeMaxCurrent))
- || (bmsSocUnderMin.isDefined() && !Objects.equals(bmsSocUnderMin.get(), setSocUnderMin))) {
+ || (bmsSocUnderMin.isDefined() && !Objects.equals(bmsSocUnderMin.get(), setSocUnderMin))
+ || (bmsOfflineSocUnderMin.isDefined()
+ && !Objects.equals(bmsOfflineSocUnderMin.get(), setOfflineSocUnderMin))) {
// Update is required
this.logInfo(this.log, "Update for PV-Master BMS Registers is required." //
+ " Voltages" //
- + " [Discharge" + bmsDischargeMinVoltage.get() + " -> " + setDischargeMinVoltage + "]" //
- + " [Charge" + bmsChargeMaxVoltage + " -> " + setChargeMaxVoltage + "]" //
+ + " [Discharge " + bmsDischargeMinVoltage.get() + " -> " + setDischargeMinVoltage + "]" //
+ + " [Charge " + bmsChargeMaxVoltage.get() + " -> " + setChargeMaxVoltage + "]" //
+ " Currents " //
+ " [Charge " + bmsChargeMaxCurrent.get() + " -> " + setChargeMaxCurrent + "]" //
+ " [Discharge " + bmsDischargeMaxCurrent.get() + " -> " + setDischargeMaxCurrent + "]" //
- + " MinSoc " //
- + " [" + bmsSocUnderMin.get() + " -> " + setSocUnderMin + "] " //
- + " [" + bmsOfflineSocUnderMin.get() + " -> " + setOfflineSocUnderMin + "]");
+ + " MinSoc [" //
+ + " [On-Grid " + bmsSocUnderMin.get() + " -> " + setSocUnderMin + "] " //
+ + " [Off-Grid " + bmsOfflineSocUnderMin.get() + " -> " + setOfflineSocUnderMin + "]");
this.writeToChannel(GoodWe.ChannelId.BATTERY_PROTOCOL_ARM, 287); // EMS-Mode
diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java
index 659a948343e..c8e64305848 100644
--- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java
+++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java
@@ -991,7 +991,10 @@ protected final ModbusProtocol defineModbusProtocol() throws OpenemsException {
ElementToChannelConverter.SCALE_FACTOR_MINUS_1), // [400*N,480*N]
m(GoodWe.ChannelId.BMS_DISCHARGE_MAX_CURRENT, new UnsignedWordElement(45355),
ElementToChannelConverter.SCALE_FACTOR_MINUS_1), // [0,1000]
- m(GoodWe.ChannelId.BMS_SOC_UNDER_MIN, new UnsignedWordElement(45356))), // [0,100]
+ m(GoodWe.ChannelId.BMS_SOC_UNDER_MIN, new UnsignedWordElement(45356)), // [0,100]
+ m(GoodWe.ChannelId.BMS_OFFLINE_DISCHARGE_MIN_VOLTAGE, new UnsignedWordElement(45357),
+ ElementToChannelConverter.SCALE_FACTOR_MINUS_1), // ), //
+ m(GoodWe.ChannelId.BMS_OFFLINE_SOC_UNDER_MIN, new UnsignedWordElement(45358))), //
// Safety Parameters
new FC16WriteRegistersTask(45400, //
diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java
index f4f2b11f964..3af174eda81 100644
--- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java
+++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java
@@ -339,7 +339,7 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId
STATE_51(Doc.of(Level.INFO).text("Battery lock")), //
STATE_52(Doc.of(Level.INFO).text("Discharge circuit fault")), //
STATE_53(Doc.of(Level.INFO).text("Charging circuit failure")), //
- STATE_54(Doc.of(Level.INFO).text("Communication failure 2")), //
+ STATE_54(Doc.of(Level.INFO).text("Communication failure")), //
STATE_55(Doc.of(Level.INFO).text("Cell high temperature 3")), //
STATE_56(Doc.of(Level.INFO).text("Discharge under voltage 3")), //
STATE_57(Doc.of(Level.INFO).text("Charging under voltage 3")), //
@@ -605,7 +605,7 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId
.unit(Unit.VOLT) //
.accessMode(AccessMode.READ_WRITE)),
BMS_OFFLINE_SOC_UNDER_MIN(Doc.of(OpenemsType.INTEGER) //
- .unit(Unit.VOLT) //
+ .unit(Unit.PERCENT) //
.accessMode(AccessMode.READ_WRITE)),
CLEAR_BATTERY_SETTING(Doc.of(OpenemsType.INTEGER) //
.accessMode(AccessMode.WRITE_ONLY)),
diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterImpl.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterImpl.java
index acee4cb41f3..be0de1cfbbb 100644
--- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterImpl.java
+++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/emergencypowermeter/GoodWeEmergencyPowerMeterImpl.java
@@ -151,13 +151,11 @@ private void calculateEnergy() {
this.calculateProductionEnergy.update(null);
this.calculateConsumptionEnergy.update(null);
} else if (activePower > 0) {
- // Sell-To-Grid
- this.calculateProductionEnergy.update(0);
- this.calculateConsumptionEnergy.update(activePower * -1);
- } else {
- // Buy-From-Grid
this.calculateProductionEnergy.update(activePower);
this.calculateConsumptionEnergy.update(0);
+ } else {
+ this.calculateProductionEnergy.update(0);
+ this.calculateConsumptionEnergy.update(activePower * -1);
}
}
diff --git a/io.openems.edge.meter.pqplus.umd97/readme.adoc b/io.openems.edge.meter.pqplus.umd97/readme.adoc
deleted file mode 100644
index f8ebe109fa0..00000000000
--- a/io.openems.edge.meter.pqplus.umd97/readme.adoc
+++ /dev/null
@@ -1,9 +0,0 @@
-= PQ Plus UMD 97 Meter
-
-Applies also to UMD 96, UMD 97, UMD 98, UMD 807, UMD 701, UMD 704, UMD 705, UMD 706, UMD 707, UMD 709, UMD 710, UMD 913, UMC 26
-
-Implemented Natures::
-- SymmetricMeter
-- AsymmetricMeter
-
-https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.meter.pqplus.umd97[Source Code icon:github[]]
\ No newline at end of file
diff --git a/io.openems.edge.meter.pqplus.umd97/.classpath b/io.openems.edge.meter.pqplus/.classpath
similarity index 100%
rename from io.openems.edge.meter.pqplus.umd97/.classpath
rename to io.openems.edge.meter.pqplus/.classpath
diff --git a/io.openems.edge.meter.pqplus.umd97/.gitignore b/io.openems.edge.meter.pqplus/.gitignore
similarity index 100%
rename from io.openems.edge.meter.pqplus.umd97/.gitignore
rename to io.openems.edge.meter.pqplus/.gitignore
diff --git a/io.openems.edge.meter.pqplus.umd97/.project b/io.openems.edge.meter.pqplus/.project
similarity index 90%
rename from io.openems.edge.meter.pqplus.umd97/.project
rename to io.openems.edge.meter.pqplus/.project
index a91bad24c5e..d8762fd65ea 100644
--- a/io.openems.edge.meter.pqplus.umd97/.project
+++ b/io.openems.edge.meter.pqplus/.project
@@ -1,6 +1,6 @@
- io.openems.edge.meter.pqplus.umd97
+ io.openems.edge.meter.pqplus
diff --git a/io.openems.edge.meter.pqplus.umd97/bnd.bnd b/io.openems.edge.meter.pqplus/bnd.bnd
similarity index 86%
rename from io.openems.edge.meter.pqplus.umd97/bnd.bnd
rename to io.openems.edge.meter.pqplus/bnd.bnd
index 9fbaa113ab4..38050940f32 100644
--- a/io.openems.edge.meter.pqplus.umd97/bnd.bnd
+++ b/io.openems.edge.meter.pqplus/bnd.bnd
@@ -1,4 +1,4 @@
-Bundle-Name: OpenEMS Edge Meter PQ Plus UMD 97
+Bundle-Name: OpenEMS Edge Meter PQ Plus
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.meter.pqplus.umd97/doc/PQPlus-Com-Protokoll_Modbus.pdf b/io.openems.edge.meter.pqplus/doc/PQPlus-Com-Protokoll_Modbus.pdf
similarity index 100%
rename from io.openems.edge.meter.pqplus.umd97/doc/PQPlus-Com-Protokoll_Modbus.pdf
rename to io.openems.edge.meter.pqplus/doc/PQPlus-Com-Protokoll_Modbus.pdf
diff --git a/io.openems.edge.meter.pqplus/doc/pqplus-com-protokoll-modbus_3_0.pdf b/io.openems.edge.meter.pqplus/doc/pqplus-com-protokoll-modbus_3_0.pdf
new file mode 100644
index 00000000000..80d83dcacd5
Binary files /dev/null and b/io.openems.edge.meter.pqplus/doc/pqplus-com-protokoll-modbus_3_0.pdf differ
diff --git a/io.openems.edge.meter.pqplus/readme.adoc b/io.openems.edge.meter.pqplus/readme.adoc
new file mode 100644
index 00000000000..079f9436692
--- /dev/null
+++ b/io.openems.edge.meter.pqplus/readme.adoc
@@ -0,0 +1,15 @@
+= PQ-Plus Meters UMD96 | UMD97
+
+Implemented Natures::
+- SymmetricMeter
+- AsymmetricMeter
+
+Details of the meter is present in
+
+UMD 96
+- https://www.pq-plus.de/produkte/hardwarekomponenten/umd-96/
+
+UMD 97
+- https://www.pq-plus.de/en/products/hardware-components/umd-97/
+
+https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.meter.pqplus[Source Code icon:github[]]
\ No newline at end of file
diff --git a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/Config.java b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/Config.java
new file mode 100644
index 00000000000..ee9703714c0
--- /dev/null
+++ b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/Config.java
@@ -0,0 +1,35 @@
+package io.openems.edge.meter.pqplus.umd96;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+import io.openems.edge.meter.api.MeterType;
+
+@ObjectClassDefinition(//
+ name = "Meter PQ-Plus UMD 96", //
+ description = "Implements the PQ-Plus UMD 96 power meter.")
+@interface Config {
+
+ @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component")
+ String id() default "meter0";
+
+ @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 = "Meter-Type", description = "What is measured by this Meter?")
+ MeterType type() default MeterType.PRODUCTION;
+
+ @AttributeDefinition(name = "Modbus-ID", description = "ID of Modbus bridge.")
+ String modbus_id() default "modbus0";
+
+ @AttributeDefinition(name = "Modbus Unit-ID", description = "The Unit-ID of the Modbus device. Defaults to '1' for Modbus/TCP.")
+ int modbusUnitId() default 1;
+
+ @AttributeDefinition(name = "Modbus target filter", description = "This is auto-generated by 'Modbus-ID'.")
+ String Modbus_target() default "(enabled=true)";
+
+ String webconsole_configurationFactory_nameHint() default "Meter PQ-Plus UMD 96 [{id}]";
+}
\ No newline at end of file
diff --git a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96.java b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96.java
new file mode 100644
index 00000000000..c41869fbd9a
--- /dev/null
+++ b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96.java
@@ -0,0 +1,24 @@
+package io.openems.edge.meter.pqplus.umd96;
+
+import io.openems.edge.bridge.modbus.api.ModbusComponent;
+import io.openems.edge.common.channel.Doc;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.meter.api.AsymmetricMeter;
+import io.openems.edge.meter.api.SymmetricMeter;
+
+public interface MeterPqplusUmd96 extends SymmetricMeter, AsymmetricMeter, ModbusComponent, OpenemsComponent {
+
+ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
+ ;
+ private final Doc doc;
+
+ private ChannelId(Doc doc) {
+ this.doc = doc;
+ }
+
+ public Doc doc() {
+ return this.doc;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96Impl.java b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96Impl.java
new file mode 100644
index 00000000000..5246de7d901
--- /dev/null
+++ b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96Impl.java
@@ -0,0 +1,134 @@
+package io.openems.edge.meter.pqplus.umd96;
+
+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 io.openems.common.exceptions.OpenemsException;
+import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent;
+import io.openems.edge.bridge.modbus.api.BridgeModbus;
+import io.openems.edge.bridge.modbus.api.ElementToChannelConverter;
+import io.openems.edge.bridge.modbus.api.ModbusComponent;
+import io.openems.edge.bridge.modbus.api.ModbusProtocol;
+import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
+import io.openems.edge.bridge.modbus.api.element.FloatDoublewordElement;
+import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.common.taskmanager.Priority;
+import io.openems.edge.meter.api.AsymmetricMeter;
+import io.openems.edge.meter.api.MeterType;
+import io.openems.edge.meter.api.SymmetricMeter;
+
+/**
+ * Implements the PQ Plus UMD 96 meter.
+ *
+ *
+ * - https://www.pq-plus.de/produkte/hardwarekomponenten/umd-96/
+ *
- https://www.pq-plus.de/site/assets/files/2795/pqplus-com-protokoll-modbus_3_0.pdf
+ *
+ */
+@Designate(ocd = Config.class, factory = true)
+@Component(//
+ name = "Meter.PqPlus.UMD96", //
+ immediate = true, //
+ configurationPolicy = ConfigurationPolicy.REQUIRE //
+)
+public class MeterPqplusUmd96Impl extends AbstractOpenemsModbusComponent
+ implements MeterPqplusUmd96, SymmetricMeter, AsymmetricMeter, ModbusComponent, OpenemsComponent {
+
+ private MeterType meterType = MeterType.PRODUCTION;
+
+ @Reference
+ private ConfigurationAdmin cm;
+
+ public MeterPqplusUmd96Impl() {
+ super(//
+ OpenemsComponent.ChannelId.values(), //
+ ModbusComponent.ChannelId.values(), //
+ SymmetricMeter.ChannelId.values(), //
+ AsymmetricMeter.ChannelId.values(), //
+ MeterPqplusUmd96.ChannelId.values() //
+ );
+ }
+
+ @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY)
+ protected void setModbus(BridgeModbus modbus) {
+ super.setModbus(modbus);
+ }
+
+ @Activate
+ void activate(ComponentContext context, Config config) throws OpenemsException {
+ this.meterType = config.type();
+
+ if (super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm,
+ "Modbus", config.modbus_id())) {
+ return;
+ }
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ super.deactivate();
+ }
+
+ @Override
+ public MeterType getMeterType() {
+ return this.meterType;
+ }
+
+ @Override
+ protected ModbusProtocol defineModbusProtocol() throws OpenemsException {
+ return new ModbusProtocol(this, //
+ // Frequency
+ new FC3ReadRegistersTask(0x1004, Priority.LOW, //
+ m(SymmetricMeter.ChannelId.FREQUENCY, new FloatDoublewordElement(0x1004),
+ ElementToChannelConverter.SCALE_FACTOR_3)),
+ // Voltages
+ new FC3ReadRegistersTask(0x1100, Priority.HIGH, //
+ m(new FloatDoublewordElement(0x1100)) //
+ .m(AsymmetricMeter.ChannelId.VOLTAGE_L1, ElementToChannelConverter.SCALE_FACTOR_3) //
+ .m(SymmetricMeter.ChannelId.VOLTAGE, ElementToChannelConverter.SCALE_FACTOR_3) //
+ .build(), //
+ m(AsymmetricMeter.ChannelId.VOLTAGE_L2, new FloatDoublewordElement(0x1102),
+ ElementToChannelConverter.SCALE_FACTOR_3),
+ m(AsymmetricMeter.ChannelId.VOLTAGE_L3, new FloatDoublewordElement(0x1104),
+ ElementToChannelConverter.SCALE_FACTOR_3)),
+ // Currents
+ new FC3ReadRegistersTask(0x1200, Priority.HIGH, //
+ m(new FloatDoublewordElement(0x1200))
+ .m(AsymmetricMeter.ChannelId.CURRENT_L1, ElementToChannelConverter.SCALE_FACTOR_3) //
+ .m(SymmetricMeter.ChannelId.CURRENT, ElementToChannelConverter.SCALE_FACTOR_3) //
+ .build(), //
+ m(AsymmetricMeter.ChannelId.CURRENT_L2, new FloatDoublewordElement(0x1202), //
+ ElementToChannelConverter.SCALE_FACTOR_3),
+ m(AsymmetricMeter.ChannelId.CURRENT_L3, new FloatDoublewordElement(0x1204), //
+ ElementToChannelConverter.SCALE_FACTOR_3)),
+ // Power values
+ new FC3ReadRegistersTask(0x1314, Priority.HIGH, //
+ m(SymmetricMeter.ChannelId.ACTIVE_POWER, new FloatDoublewordElement(0x1314)),
+ m(SymmetricMeter.ChannelId.REACTIVE_POWER, new FloatDoublewordElement(0x1316)), //
+ new DummyRegisterElement(0x1318, 0x131F), //
+ m(AsymmetricMeter.ChannelId.ACTIVE_POWER_L1, new FloatDoublewordElement(0x1320)),
+ m(AsymmetricMeter.ChannelId.ACTIVE_POWER_L2, new FloatDoublewordElement(0x1322)),
+ m(AsymmetricMeter.ChannelId.ACTIVE_POWER_L3, new FloatDoublewordElement(0x1324)),
+ new DummyRegisterElement(0x1326, 0x1327), //
+ m(AsymmetricMeter.ChannelId.REACTIVE_POWER_L1, new FloatDoublewordElement(0x1328)),
+ m(AsymmetricMeter.ChannelId.REACTIVE_POWER_L2, new FloatDoublewordElement(0x132A)),
+ m(AsymmetricMeter.ChannelId.REACTIVE_POWER_L3, new FloatDoublewordElement(0x132C)))//
+ );
+
+ }
+
+ @Override
+ public String debugLog() {
+ return "L:" + this.getActivePower().asString();
+ }
+}
diff --git a/io.openems.edge.meter.pqplus.umd97/src/io/openems/edge/meter/pqplus/umd97/Config.java b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/Config.java
similarity index 98%
rename from io.openems.edge.meter.pqplus.umd97/src/io/openems/edge/meter/pqplus/umd97/Config.java
rename to io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/Config.java
index a5cce62db9f..6f158db2353 100644
--- a/io.openems.edge.meter.pqplus.umd97/src/io/openems/edge/meter/pqplus/umd97/Config.java
+++ b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/Config.java
@@ -5,7 +5,7 @@
import io.openems.edge.meter.api.MeterType;
-@ObjectClassDefinition( //
+@ObjectClassDefinition(//
name = "Meter PQ-Plus UMD 97", //
description = "Implements the PQ-Plus UMD 97 power meter.")
@interface Config {
diff --git a/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97.java b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97.java
new file mode 100644
index 00000000000..e0efffe443f
--- /dev/null
+++ b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97.java
@@ -0,0 +1,24 @@
+package io.openems.edge.meter.pqplus.umd97;
+
+import io.openems.edge.bridge.modbus.api.ModbusComponent;
+import io.openems.edge.common.channel.Doc;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.meter.api.AsymmetricMeter;
+import io.openems.edge.meter.api.SymmetricMeter;
+
+public interface MeterPqplusUmd97 extends SymmetricMeter, AsymmetricMeter, ModbusComponent, OpenemsComponent {
+
+ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
+ ;
+ private final Doc doc;
+
+ private ChannelId(Doc doc) {
+ this.doc = doc;
+ }
+
+ public Doc doc() {
+ return this.doc;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.meter.pqplus.umd97/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97.java b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Impl.java
similarity index 89%
rename from io.openems.edge.meter.pqplus.umd97/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97.java
rename to io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Impl.java
index db755ede2a5..f60d5180028 100644
--- a/io.openems.edge.meter.pqplus.umd97/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97.java
+++ b/io.openems.edge.meter.pqplus/src/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Impl.java
@@ -21,7 +21,6 @@
import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
import io.openems.edge.bridge.modbus.api.element.FloatDoublewordElement;
import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask;
-import io.openems.edge.common.channel.Doc;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.taskmanager.Priority;
import io.openems.edge.meter.api.AsymmetricMeter;
@@ -31,25 +30,30 @@
/**
* Implements the PQ Plus UMD 97 meter.
*
+ *
* https://www.pq-plus.de/news/pqplus/umd-97-messgeraet.html
*/
@Designate(ocd = Config.class, factory = true)
-@Component(name = "Meter.PqPlus.UMD97", immediate = true, configurationPolicy = ConfigurationPolicy.REQUIRE)
-public class MeterPqplusUmd97 extends AbstractOpenemsModbusComponent
- implements SymmetricMeter, AsymmetricMeter, ModbusComponent, OpenemsComponent {
+@Component(//
+ name = "Meter.PqPlus.UMD97", //
+ immediate = true, //
+ configurationPolicy = ConfigurationPolicy.REQUIRE //
+)
+public class MeterPqplusUmd97Impl extends AbstractOpenemsModbusComponent
+ implements MeterPqplusUmd97, SymmetricMeter, AsymmetricMeter, ModbusComponent, OpenemsComponent {
private MeterType meterType = MeterType.PRODUCTION;
@Reference
- protected ConfigurationAdmin cm;
+ private ConfigurationAdmin cm;
- public MeterPqplusUmd97() {
+ public MeterPqplusUmd97Impl() {
super(//
OpenemsComponent.ChannelId.values(), //
ModbusComponent.ChannelId.values(), //
SymmetricMeter.ChannelId.values(), //
AsymmetricMeter.ChannelId.values(), //
- ChannelId.values() //
+ MeterPqplusUmd97.ChannelId.values() //
);
}
@@ -73,19 +77,6 @@ protected void deactivate() {
super.deactivate();
}
- public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
- ;
- private final Doc doc;
-
- private ChannelId(Doc doc) {
- this.doc = doc;
- }
-
- public Doc doc() {
- return this.doc;
- }
- }
-
@Override
public MeterType getMeterType() {
return this.meterType;
diff --git a/io.openems.edge.meter.pqplus.umd97/test/.gitignore b/io.openems.edge.meter.pqplus/test/.gitignore
similarity index 100%
rename from io.openems.edge.meter.pqplus.umd97/test/.gitignore
rename to io.openems.edge.meter.pqplus/test/.gitignore
diff --git a/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96Test.java b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96Test.java
new file mode 100644
index 00000000000..b233a26564d
--- /dev/null
+++ b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MeterPqplusUmd96Test.java
@@ -0,0 +1,28 @@
+package io.openems.edge.meter.pqplus.umd96;
+
+import org.junit.Test;
+
+import io.openems.edge.bridge.modbus.test.DummyModbusBridge;
+import io.openems.edge.common.test.ComponentTest;
+import io.openems.edge.common.test.DummyConfigurationAdmin;
+import io.openems.edge.meter.api.MeterType;
+
+public class MeterPqplusUmd96Test {
+
+ private static final String METER_ID = "meter0";
+ private static final String MODBUS_ID = "modbus0";
+
+ @Test
+ public void test() throws Exception {
+ new ComponentTest(new MeterPqplusUmd96Impl()) //
+ .addReference("cm", new DummyConfigurationAdmin()) //
+ .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) //
+ .activate(MyConfig.create() //
+ .setId(METER_ID) //
+ .setModbusId(MODBUS_ID) //
+ .setType(MeterType.GRID) //
+ .build()) //
+ ;
+ }
+
+}
diff --git a/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MyConfig.java b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MyConfig.java
new file mode 100644
index 00000000000..fa40fbc58c7
--- /dev/null
+++ b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd96/MyConfig.java
@@ -0,0 +1,75 @@
+package io.openems.edge.meter.pqplus.umd96;
+
+import io.openems.common.utils.ConfigUtils;
+import io.openems.edge.common.test.AbstractComponentConfig;
+import io.openems.edge.meter.api.MeterType;
+
+@SuppressWarnings("all")
+public class MyConfig extends AbstractComponentConfig implements Config {
+
+ protected static class Builder {
+ private String id;
+ private String modbusId;
+ public int modbusUnitId;
+ public MeterType type;
+
+ private Builder() {
+ }
+
+ public Builder setId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setModbusId(String modbusId) {
+ this.modbusId = modbusId;
+ return this;
+ }
+
+ public Builder setType(MeterType type) {
+ this.type = type;
+ 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 modbus_id() {
+ return this.builder.modbusId;
+ }
+
+ @Override
+ public String Modbus_target() {
+ return ConfigUtils.generateReferenceTargetFilter(this.id(), this.modbus_id());
+ }
+
+ @Override
+ public int modbusUnitId() {
+ return this.builder.modbusUnitId;
+ }
+
+ @Override
+ public MeterType type() {
+ return this.builder.type;
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.meter.pqplus.umd97/test/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Test.java b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Test.java
similarity index 93%
rename from io.openems.edge.meter.pqplus.umd97/test/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Test.java
rename to io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Test.java
index 1af10aaf467..13e045190c2 100644
--- a/io.openems.edge.meter.pqplus.umd97/test/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Test.java
+++ b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MeterPqplusUmd97Test.java
@@ -14,7 +14,7 @@ public class MeterPqplusUmd97Test {
@Test
public void test() throws Exception {
- new ComponentTest(new MeterPqplusUmd97()) //
+ new ComponentTest(new MeterPqplusUmd97Impl()) //
.addReference("cm", new DummyConfigurationAdmin()) //
.addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) //
.activate(MyConfig.create() //
@@ -24,4 +24,5 @@ public void test() throws Exception {
.build()) //
;
}
-}
\ No newline at end of file
+
+}
diff --git a/io.openems.edge.meter.pqplus.umd97/test/io/openems/edge/meter/pqplus/umd97/MyConfig.java b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MyConfig.java
similarity index 95%
rename from io.openems.edge.meter.pqplus.umd97/test/io/openems/edge/meter/pqplus/umd97/MyConfig.java
rename to io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MyConfig.java
index 61722c52782..a5ce3774014 100644
--- a/io.openems.edge.meter.pqplus.umd97/test/io/openems/edge/meter/pqplus/umd97/MyConfig.java
+++ b/io.openems.edge.meter.pqplus/test/io/openems/edge/meter/pqplus/umd97/MyConfig.java
@@ -8,8 +8,8 @@
public class MyConfig extends AbstractComponentConfig implements Config {
protected static class Builder {
- private String id = null;
- private String modbusId = null;
+ private String id;
+ private String modbusId;
public int modbusUnitId;
public MeterType type;
diff --git a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/RecordWorker.java b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/RecordWorker.java
index 265a177b420..22edc9cc19c 100644
--- a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/RecordWorker.java
+++ b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/RecordWorker.java
@@ -220,6 +220,7 @@ private Function getChannelAggregateFunction(Unit
case AMPERE_HOURS:
case DEGREE_CELSIUS:
case DEZIDEGREE_CELSIUS:
+ case EUROS_PER_MEGAWATT_HOUR:
case HERTZ:
case HOUR:
case KILOAMPERE_HOURS:
diff --git a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImpl.java b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImpl.java
index 266f55e37fd..719e19b5d6f 100644
--- a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImpl.java
+++ b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImpl.java
@@ -487,6 +487,7 @@ private ChannelDef getDsDefForChannel(Unit channelUnit) {
case AMPERE_HOURS:
case DEGREE_CELSIUS:
case DEZIDEGREE_CELSIUS:
+ case EUROS_PER_MEGAWATT_HOUR:
case HERTZ:
case HOUR:
case KILOAMPERE_HOURS:
diff --git a/io.openems.edge.timeofusetariff.api/.classpath b/io.openems.edge.timeofusetariff.api/.classpath
new file mode 100644
index 00000000000..7a6fc254361
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/.classpath
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/io.openems.edge.timeofusetariff.api/.gitignore b/io.openems.edge.timeofusetariff.api/.gitignore
new file mode 100644
index 00000000000..c2b941a96de
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/.gitignore
@@ -0,0 +1,2 @@
+/bin_test/
+/generated/
diff --git a/io.openems.edge.timeofusetariff.api/.project b/io.openems.edge.timeofusetariff.api/.project
new file mode 100644
index 00000000000..bb2aa4912dd
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/.project
@@ -0,0 +1,23 @@
+
+
+ io.openems.edge.timeofusetariff.api
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ bndtools.core.bndbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ bndtools.core.bndnature
+
+
diff --git a/io.openems.edge.timeofusetariff.api/bnd.bnd b/io.openems.edge.timeofusetariff.api/bnd.bnd
new file mode 100644
index 00000000000..bef6d48d25c
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/bnd.bnd
@@ -0,0 +1,12 @@
+Bundle-Name: OpenEMS Edge Time-Of-Use Tariff API
+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,\
+
+-testpath: \
+ ${testpath}
diff --git a/io.openems.edge.timeofusetariff.api/readme.adoc b/io.openems.edge.timeofusetariff.api/readme.adoc
new file mode 100644
index 00000000000..702b5dbbce0
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/readme.adoc
@@ -0,0 +1,7 @@
+= Time-Of-Use Tariff API.
+
+Provides abstract access for getting prices from every "Time-Of-Use" tariff providers like:
+- aWATTar
+- Stromdao with Corrently
+
+https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.timeofusetariff.api[Source Code icon:github[]]
\ No newline at end of file
diff --git a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/TimeOfUsePrices.java b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/TimeOfUsePrices.java
new file mode 100644
index 00000000000..b417063635d
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/TimeOfUsePrices.java
@@ -0,0 +1,57 @@
+package io.openems.edge.timeofusetariff.api;
+
+import java.time.ZonedDateTime;
+
+/**
+ * Holds time of use prices for 24 h and the time when it is retrieved; //
+ * prices are one value per 15 minutes; 96 values in total.
+ *
+ *
+ * Values have unit EUR/MWh.
+ */
+public class TimeOfUsePrices {
+
+ public final static int NUMBER_OF_VALUES = 96;
+
+ private final ZonedDateTime updateTime;
+
+ private final Float[] values = new Float[NUMBER_OF_VALUES];
+
+ /**
+ * Constructs a {@link TimeOfUsePrices}.
+ *
+ * @param updateTime Retrieved time of the prices.
+ * @param values the 96 quarterly price values[24 hours].
+ */
+ public TimeOfUsePrices(ZonedDateTime updateTime, Float... values) {
+ super();
+ for (int i = 0; i < NUMBER_OF_VALUES && i < values.length; i++) {
+ this.values[i] = values[i];
+ }
+ this.updateTime = updateTime;
+ }
+
+ /**
+ * Gives electricity prices for the next 24 h; one value per 15 minutes; 96
+ * values in total.
+ *
+ *
+ * E.g. if called at 10:05, the first value stands for 10:00 to 10:15; second
+ * value for 10:15 to 10:30.
+ *
+ * @return the prices
+ */
+ public Float[] getValues() {
+ return this.values;
+ }
+
+ /**
+ * Gets the time of the last update of prices.
+ *
+ * @return the time
+ */
+ public ZonedDateTime getUpdateTime() {
+ return this.updateTime;
+ }
+
+}
diff --git a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/TimeOfUseTariff.java b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/TimeOfUseTariff.java
new file mode 100644
index 00000000000..f06f6f93919
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/TimeOfUseTariff.java
@@ -0,0 +1,24 @@
+package io.openems.edge.timeofusetariff.api;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Provides a prediction for the next 24 h; one value per 15 minutes; 96 values
+ * in total.
+ */
+@ProviderType
+public interface TimeOfUseTariff {
+
+ /**
+ * Gives electricity prices for the next 24 h; one value per 15 minutes; 96
+ * values in total.
+ *
+ *
+ * E.g. if called at 10:05, the first value stands for 10:00 to 10:15; second
+ * value for 10:15 to 10:30.
+ *
+ * @return the prices
+ */
+ public TimeOfUsePrices getPrices();
+
+}
diff --git a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/package-info.java b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/package-info.java
new file mode 100644
index 00000000000..01093ab3b54
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/package-info.java
@@ -0,0 +1,3 @@
+@org.osgi.annotation.versioning.Version("1.0.0")
+@org.osgi.annotation.bundle.Export
+package io.openems.edge.timeofusetariff.api;
diff --git a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/TimeOfUseTariffUtils.java b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/TimeOfUseTariffUtils.java
new file mode 100644
index 00000000000..1ee6e26ec7c
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/TimeOfUseTariffUtils.java
@@ -0,0 +1,54 @@
+package io.openems.edge.timeofusetariff.api.utils;
+
+import java.time.Clock;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoField;
+import java.time.temporal.ChronoUnit;
+
+import com.google.common.collect.ImmutableSortedMap;
+
+import io.openems.edge.timeofusetariff.api.TimeOfUsePrices;
+
+public class TimeOfUseTariffUtils {
+
+ /**
+ * Returns the Array of 24 hour [96 quarterly] electricity prices in EUR/MWh.
+ *
+ * @param clock the {@link Clock}
+ * @param priceMap {@link ImmutableSortedMap} with quarterly Time stamps
+ * and the price.
+ * @param updateTimeStamp time when prices are retrieved.
+ * @return the quarterly prices of next 24 hours along with the time they are
+ * retrieved.
+ */
+ public static TimeOfUsePrices getNext24HourPrices(Clock clock, ImmutableSortedMap priceMap,
+ Clock updateTimeStamp) {
+
+ ZonedDateTime updateTime = getNowRoundedDownToMinutes(updateTimeStamp, 15);
+
+ // Returns the empty array if the map is empty.
+ if (priceMap.isEmpty()) {
+ return new TimeOfUsePrices(updateTime);
+ }
+
+ ZonedDateTime now = getNowRoundedDownToMinutes(clock, 15);
+ // Converts the map values to array.
+ // if the map size is less than 96, rest of the values will store as null.
+ final Float[] priceList = priceMap.tailMap(now).values().toArray(new Float[TimeOfUsePrices.NUMBER_OF_VALUES]);
+
+ return new TimeOfUsePrices(updateTime, priceList);
+ }
+
+ /**
+ * Gets 'now' from the Clock and rounds it down to required minutes.
+ *
+ * @param clock the {@link Clock}
+ * @param minutes the custom minutes to roundoff to.
+ * @return the rounded result
+ */
+ public static ZonedDateTime getNowRoundedDownToMinutes(Clock clock, int minutes) {
+ ZonedDateTime d = ZonedDateTime.now(clock);
+ int minuteOfDay = d.get(ChronoField.MINUTE_OF_DAY);
+ return d.with(ChronoField.NANO_OF_DAY, 0).plus(minuteOfDay / minutes * minutes, ChronoUnit.MINUTES);
+ }
+}
diff --git a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/package-info.java b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/package-info.java
new file mode 100644
index 00000000000..dbad637321c
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/api/utils/package-info.java
@@ -0,0 +1,3 @@
+@org.osgi.annotation.versioning.Version("1.0.0")
+@org.osgi.annotation.bundle.Export
+package io.openems.edge.timeofusetariff.api.utils;
diff --git a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/test/DummyTimeOfUseTariffProvider.java b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/test/DummyTimeOfUseTariffProvider.java
new file mode 100644
index 00000000000..f427362d22c
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/test/DummyTimeOfUseTariffProvider.java
@@ -0,0 +1,39 @@
+package io.openems.edge.timeofusetariff.test;
+
+import java.time.ZonedDateTime;
+import io.openems.edge.timeofusetariff.api.TimeOfUsePrices;
+import io.openems.edge.timeofusetariff.api.TimeOfUseTariff;
+
+public class DummyTimeOfUseTariffProvider implements TimeOfUseTariff {
+
+ private final TimeOfUsePrices prices;
+
+ /**
+ * Builds a {@link DummyTimeOfUseTariffProvider} from hourly prices.
+ *
+ * @param hourlyPrices an array of hourly prices
+ * @return a {@link DummyTimeOfUseTariffProvider}
+ */
+ public static DummyTimeOfUseTariffProvider fromHourlyPrices(ZonedDateTime now, Float[] hourlyPrices) {
+ Float[] quarterlyPrices = new Float[96];
+
+ for (int i = 0; i < 24; i++) {
+ quarterlyPrices[i] = hourlyPrices[i];
+ quarterlyPrices[i + 1] = hourlyPrices[i];
+ quarterlyPrices[i + 2] = hourlyPrices[i];
+ quarterlyPrices[i + 3] = hourlyPrices[i];
+ }
+
+ return new DummyTimeOfUseTariffProvider(now, quarterlyPrices);
+ }
+
+ private DummyTimeOfUseTariffProvider(ZonedDateTime now, Float[] quarterlyPrices) {
+ this.prices = new TimeOfUsePrices(now, quarterlyPrices);
+ }
+
+ @Override
+ public TimeOfUsePrices getPrices() {
+ return this.prices;
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/test/package-info.java b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/test/package-info.java
new file mode 100644
index 00000000000..785b1b54bb7
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/test/package-info.java
@@ -0,0 +1,3 @@
+@org.osgi.annotation.versioning.Version("1.0.0")
+@org.osgi.annotation.bundle.Export
+package io.openems.edge.timeofusetariff.test;
diff --git a/io.openems.edge.timeofusetariff.api/test/.gitignore b/io.openems.edge.timeofusetariff.api/test/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/io.openems.edge.timeofusetariff.awattar/.classpath b/io.openems.edge.timeofusetariff.awattar/.classpath
new file mode 100644
index 00000000000..7a6fc254361
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.awattar/.classpath
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/io.openems.edge.timeofusetariff.awattar/.gitignore b/io.openems.edge.timeofusetariff.awattar/.gitignore
new file mode 100644
index 00000000000..c2b941a96de
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.awattar/.gitignore
@@ -0,0 +1,2 @@
+/bin_test/
+/generated/
diff --git a/io.openems.edge.timeofusetariff.awattar/.project b/io.openems.edge.timeofusetariff.awattar/.project
new file mode 100644
index 00000000000..344077bede4
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.awattar/.project
@@ -0,0 +1,23 @@
+
+
+ io.openems.edge.timeofusetariff.awattar
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ bndtools.core.bndbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ bndtools.core.bndnature
+
+
diff --git a/io.openems.edge.timeofusetariff.awattar/bnd.bnd b/io.openems.edge.timeofusetariff.awattar/bnd.bnd
new file mode 100644
index 00000000000..a509eceeba4
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.awattar/bnd.bnd
@@ -0,0 +1,14 @@
+Bundle-Name: OpenEMS Edge Time-Of-Use Tariff Awattar
+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.timeofusetariff.api,\
+ io.openems.wrapper.okhttp,\
+
+-testpath: \
+ ${testpath}
\ No newline at end of file
diff --git a/io.openems.edge.timeofusetariff.awattar/readme.adoc b/io.openems.edge.timeofusetariff.awattar/readme.adoc
new file mode 100644
index 00000000000..ec3a8a13aa8
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.awattar/readme.adoc
@@ -0,0 +1,5 @@
+= Time-Of-Use Tariff Awattar
+
+Retrieves the hourly prices from the Awattar API and converts them into quarterly prices. Prices are updated every day at 14:00 and stored locally.
+
+https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.timeofusetariff.awattar[Source Code icon:github[]]
\ No newline at end of file
diff --git a/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/Awattar.java b/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/Awattar.java
new file mode 100644
index 00000000000..0b79bbcbeb7
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/Awattar.java
@@ -0,0 +1,26 @@
+package io.openems.edge.timeofusetariff.awattar;
+
+import io.openems.common.types.OpenemsType;
+import io.openems.edge.common.channel.Doc;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.timeofusetariff.api.TimeOfUseTariff;
+
+public interface Awattar extends TimeOfUseTariff, OpenemsComponent {
+
+ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
+ HTTP_STATUS_CODE(Doc.of(OpenemsType.INTEGER)//
+ .text("Displays the HTTP status code"))//
+ ;
+
+ private final Doc doc;
+
+ private ChannelId(Doc doc) {
+ this.doc = doc;
+ }
+
+ @Override
+ public Doc doc() {
+ return this.doc;
+ }
+ }
+}
diff --git a/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/AwattarImpl.java b/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/AwattarImpl.java
new file mode 100644
index 00000000000..4f52816b793
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/AwattarImpl.java
@@ -0,0 +1,175 @@
+package io.openems.edge.timeofusetariff.awattar;
+
+import java.io.IOException;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.TreeMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+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.metatype.annotations.Designate;
+
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.common.utils.JsonUtils;
+import io.openems.common.utils.ThreadPoolUtils;
+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.timeofusetariff.api.TimeOfUsePrices;
+import io.openems.edge.timeofusetariff.api.TimeOfUseTariff;
+import io.openems.edge.timeofusetariff.api.utils.TimeOfUseTariffUtils;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+@Designate(ocd = Config.class, factory = true)
+@Component(//
+ name = "TimeOfUseTariff.Awattar", //
+ immediate = true, //
+ configurationPolicy = ConfigurationPolicy.REQUIRE //
+)
+public class AwattarImpl extends AbstractOpenemsComponent implements TimeOfUseTariff, OpenemsComponent, Awattar {
+
+ private static final String AWATTAR_API_URL = "https://api.awattar.com/v1/marketdata";
+
+ private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+
+ private final AtomicReference> prices = new AtomicReference>(
+ ImmutableSortedMap.of());
+
+ private Clock updateTimeStamp = Clock.fixed(Instant.MIN, ZoneId.systemDefault());
+
+ private final Runnable task = () -> {
+
+ /*
+ * Update Map of prices
+ */
+ ImmutableSortedMap prices;
+ try {
+ OkHttpClient client = new OkHttpClient();
+ Request request = new Request.Builder() //
+ .url(AWATTAR_API_URL) //
+ // aWATTar currently does not anymore require an Apikey.
+ // .header("Authorization", Credentials.basic(apikey, "")) //
+ .build();
+
+ Response response = client.newCall(request).execute();
+ this.channel(Awattar.ChannelId.HTTP_STATUS_CODE).setNextValue(response.code());
+
+ if (!response.isSuccessful()) {
+ throw new IOException("Unexpected code " + response);
+ }
+
+ // Parse the response for the prices
+ prices = AwattarImpl.parsePrices(response.body().toString());
+
+ // store the time stamp
+ this.updateTimeStamp = Clock.systemDefaultZone();
+ } catch (IOException | OpenemsNamedException e) {
+ e.printStackTrace();
+ prices = ImmutableSortedMap.of();
+ // TODO Try again in x minutes
+ }
+ this.prices.set(prices);
+
+ /*
+ * Schedule next price update for 2 pm
+ */
+ ZonedDateTime now = ZonedDateTime.now();
+ ZonedDateTime nextRun = now.withHour(14).truncatedTo(ChronoUnit.HOURS);
+ if (now.isAfter(nextRun)) {
+ nextRun = nextRun.plusDays(1);
+ }
+
+ Duration duration = Duration.between(now, nextRun);
+ long delay = duration.getSeconds();
+
+ this.executor.schedule(this.task, delay, TimeUnit.SECONDS);
+ };
+
+ @Reference
+ private ComponentManager componentManager;
+
+ public AwattarImpl() {
+ super(//
+ OpenemsComponent.ChannelId.values(), //
+ Awattar.ChannelId.values() //
+ );
+ }
+
+ @Activate
+ void activate(ComponentContext context, Config config) {
+ super.activate(context, config.id(), config.alias(), config.enabled());
+
+ if (!config.enabled()) {
+ return;
+ }
+
+ this.executor.schedule(this.task, 0, TimeUnit.SECONDS);
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ super.deactivate();
+ ThreadPoolUtils.shutdownAndAwaitTermination(this.executor, 0);
+ }
+
+ @Override
+ public TimeOfUsePrices getPrices() {
+ return TimeOfUseTariffUtils.getNext24HourPrices(Clock.systemDefaultZone() /* can be mocked for testing */,
+ this.prices.get(), this.updateTimeStamp);
+ }
+
+ /**
+ * Parse the aWATTar JSON to the Price Map.
+ *
+ * @param jsonData the aWATTar JSON
+ * @return the Price Map
+ * @throws OpenemsNamedException on error
+ */
+ public static ImmutableSortedMap parsePrices(String jsonData) throws OpenemsNamedException {
+ TreeMap result = new TreeMap<>();
+
+ if (!jsonData.isEmpty()) {
+
+ JsonObject line = JsonUtils.getAsJsonObject(JsonUtils.parse(jsonData));
+ JsonArray data = JsonUtils.getAsJsonArray(line, "data");
+
+ for (JsonElement element : data) {
+
+ float marketPrice = JsonUtils.getAsFloat(element, "marketprice");
+ long startTimestampLong = JsonUtils.getAsLong(element, "start_timestamp");
+
+ // Converting Long time stamp to ZonedDateTime.
+ ZonedDateTime startTimeStamp = ZonedDateTime //
+ .ofInstant(Instant.ofEpochMilli(startTimestampLong), ZoneId.systemDefault())
+ .truncatedTo(ChronoUnit.HOURS);
+
+ // Adding the values in the Map.
+ result.put(startTimeStamp, marketPrice);
+ result.put(startTimeStamp.plusMinutes(15), marketPrice);
+ result.put(startTimeStamp.plusMinutes(30), marketPrice);
+ result.put(startTimeStamp.plusMinutes(45), marketPrice);
+ }
+ }
+ return ImmutableSortedMap.copyOf(result);
+ }
+
+}
diff --git a/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/Config.java b/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/Config.java
new file mode 100644
index 00000000000..0b0810cf355
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/Config.java
@@ -0,0 +1,21 @@
+package io.openems.edge.timeofusetariff.awattar;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+@ObjectClassDefinition(//
+ name = "Time-Of-Use Tariff Awattar", //
+ description = "Time-Of-Use Tariff implementation for aWATTar.")
+@interface Config {
+
+ @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component")
+ String id() default "timeOfUseTariff0";
+
+ @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;
+
+ String webconsole_configurationFactory_nameHint() default "Time-Of-Use Tariff Awattar [{id}]";
+}
diff --git a/io.openems.edge.timeofusetariff.awattar/test/.gitignore b/io.openems.edge.timeofusetariff.awattar/test/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/io.openems.edge.timeofusetariff.awattar/test/io/openems/edge/timeofusetariff/awattar/AwattarProviderTest.java b/io.openems.edge.timeofusetariff.awattar/test/io/openems/edge/timeofusetariff/awattar/AwattarProviderTest.java
new file mode 100644
index 00000000000..6dd772e8d56
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.awattar/test/io/openems/edge/timeofusetariff/awattar/AwattarProviderTest.java
@@ -0,0 +1,99 @@
+package io.openems.edge.timeofusetariff.awattar;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.time.ZonedDateTime;
+import java.util.SortedMap;
+
+import org.junit.Test;
+
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.edge.common.test.ComponentTest;
+
+public class AwattarProviderTest {
+
+ private static final String CTRL_ID = "ctrl0";
+
+ @Test
+ public void test() throws Exception {
+ new ComponentTest(new AwattarImpl()) //
+ .activate(MyConfig.create() //
+ .setId(CTRL_ID) //
+ .build()) //
+ ;
+ }
+
+ @Test
+ public void nonEmptyStringTest() throws OpenemsNamedException {
+ // Parsing with custom data
+ SortedMap prices = AwattarImpl.parsePrices("{" + " \"object\": \"list\","
+ + " \"data\": [" + " {" + " \"start_timestamp\": 1632402000000,"
+ + " \"end_timestamp\": 1632405600000," + " \"marketprice\": 158.95,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632405600000,"
+ + " \"end_timestamp\": 1632409200000," + " \"marketprice\": 160.98,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632409200000,"
+ + " \"end_timestamp\": 1632412800000," + " \"marketprice\": 171.15,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632412800000,"
+ + " \"end_timestamp\": 1632416400000," + " \"marketprice\": 174.96,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632416400000,"
+ + " \"end_timestamp\": 1632420000000," + " \"marketprice\": 161.53,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632420000000,"
+ + " \"end_timestamp\": 1632423600000," + " \"marketprice\": 152,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632423600000,"
+ + " \"end_timestamp\": 1632427200000," + " \"marketprice\": 120.01,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632427200000,"
+ + " \"end_timestamp\": 1632430800000," + " \"marketprice\": 111.03,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632430800000,"
+ + " \"end_timestamp\": 1632434400000," + " \"marketprice\": 105.04,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632434400000,"
+ + " \"end_timestamp\": 1632438000000," + " \"marketprice\": 105,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632438000000,"
+ + " \"end_timestamp\": 1632441600000," + " \"marketprice\": 74.23,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632441600000,"
+ + " \"end_timestamp\": 1632445200000," + " \"marketprice\": 73.28,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632445200000,"
+ + " \"end_timestamp\": 1632448800000," + " \"marketprice\": 67.97,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632448800000,"
+ + " \"end_timestamp\": 1632452400000," + " \"marketprice\": 72.53,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632452400000,"
+ + " \"end_timestamp\": 1632456000000," + " \"marketprice\": 89.66,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632456000000,"
+ + " \"end_timestamp\": 1632459600000," + " \"marketprice\": 150.1,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632459600000,"
+ + " \"end_timestamp\": 1632463200000," + " \"marketprice\": 173.54,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632463200000,"
+ + " \"end_timestamp\": 1632466800000," + " \"marketprice\": 178.4,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632466800000,"
+ + " \"end_timestamp\": 1632470400000," + " \"marketprice\": 158.91,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632470400000,"
+ + " \"end_timestamp\": 1632474000000," + " \"marketprice\": 140.01,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632474000000,"
+ + " \"end_timestamp\": 1632477600000," + " \"marketprice\": 149.99,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632477600000,"
+ + " \"end_timestamp\": 1632481200000," + " \"marketprice\": 157.43,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632481200000,"
+ + " \"end_timestamp\": 1632484800000," + " \"marketprice\": 130.9,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632484800000,"
+ + " \"end_timestamp\": 1632488400000," + " \"marketprice\": 120.14,"
+ + " \"unit\": \"Eur/MWh\"" + " }" + " ]," + " \"url\": \"/at/v1/marketdata\"" + "}"); //
+
+ // To check if the Map is not empty
+ assertFalse(prices.isEmpty());
+
+ // To check if the a value input from the string is present in map.
+ assertTrue(prices.containsValue(120.14f));
+
+ }
+
+ @Test
+ public void emptyStringTest() throws OpenemsNamedException {
+ // Parsing with empty string
+ SortedMap prices = AwattarImpl.parsePrices("");
+
+ // To check if the map is empty.
+ assertTrue(prices.isEmpty());
+
+ }
+
+}
diff --git a/io.openems.edge.timeofusetariff.awattar/test/io/openems/edge/timeofusetariff/awattar/MyConfig.java b/io.openems.edge.timeofusetariff.awattar/test/io/openems/edge/timeofusetariff/awattar/MyConfig.java
new file mode 100644
index 00000000000..a84f0ac5f0d
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.awattar/test/io/openems/edge/timeofusetariff/awattar/MyConfig.java
@@ -0,0 +1,41 @@
+package io.openems.edge.timeofusetariff.awattar;
+
+import io.openems.edge.common.test.AbstractComponentConfig;
+
+@SuppressWarnings("all")
+public class MyConfig extends AbstractComponentConfig implements Config {
+
+ public static class Builder {
+ private String id;
+ public int zipcode;
+
+ private Builder() {
+ }
+
+ public Builder setId(String id) {
+ this.id = id;
+ 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;
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.timeofusetariff.corrently/.classpath b/io.openems.edge.timeofusetariff.corrently/.classpath
new file mode 100644
index 00000000000..7a6fc254361
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.corrently/.classpath
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/io.openems.edge.timeofusetariff.corrently/.gitignore b/io.openems.edge.timeofusetariff.corrently/.gitignore
new file mode 100644
index 00000000000..c2b941a96de
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.corrently/.gitignore
@@ -0,0 +1,2 @@
+/bin_test/
+/generated/
diff --git a/io.openems.edge.timeofusetariff.corrently/.project b/io.openems.edge.timeofusetariff.corrently/.project
new file mode 100644
index 00000000000..c425cca64b6
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.corrently/.project
@@ -0,0 +1,23 @@
+
+
+ io.openems.edge.timeofusetariff.corrently
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ bndtools.core.bndbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ bndtools.core.bndnature
+
+
diff --git a/io.openems.edge.timeofusetariff.corrently/bnd.bnd b/io.openems.edge.timeofusetariff.corrently/bnd.bnd
new file mode 100644
index 00000000000..1d7bd8d6663
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.corrently/bnd.bnd
@@ -0,0 +1,14 @@
+Bundle-Name: OpenEMS Edge Time-Of-Use Tariff Corrently by STROMDAO
+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.timeofusetariff.api,\
+ io.openems.wrapper.okhttp,\
+
+-testpath: \
+ ${testpath}
\ No newline at end of file
diff --git a/io.openems.edge.timeofusetariff.corrently/readme.adoc b/io.openems.edge.timeofusetariff.corrently/readme.adoc
new file mode 100644
index 00000000000..877a3710305
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.corrently/readme.adoc
@@ -0,0 +1,5 @@
+= Time-Of-Use Tariff Corrently by STROMDAO
+
+Retrieves the hourly prices from the Corrently API and converts them into quarterly prices. Prices are updated every day at 14:00 and stored locally.
+
+https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.timeofusetariff.corrently[Source Code icon:github[]]
\ No newline at end of file
diff --git a/io.openems.edge.timeofusetariff.corrently/src/io/openems/edge/timeofusetariff/corrently/Config.java b/io.openems.edge.timeofusetariff.corrently/src/io/openems/edge/timeofusetariff/corrently/Config.java
new file mode 100644
index 00000000000..f6950b77299
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.corrently/src/io/openems/edge/timeofusetariff/corrently/Config.java
@@ -0,0 +1,24 @@
+package io.openems.edge.timeofusetariff.corrently;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+@ObjectClassDefinition(//
+ name = "Time-Of-Use Tariff Corrently", //
+ description = "Time-Of-Use Tariff implementation for Corrently.")
+@interface Config {
+
+ @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component")
+ String id() default "timeOfUseTariff0";
+
+ @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 = "ZIP Code", description = "ZIP Code of the customer location")
+ int zipcode() default 94469;
+
+ String webconsole_configurationFactory_nameHint() default "Time-Of-Use Tariff Corrently [{id}]";
+}
\ No newline at end of file
diff --git a/io.openems.edge.timeofusetariff.corrently/src/io/openems/edge/timeofusetariff/corrently/Corrently.java b/io.openems.edge.timeofusetariff.corrently/src/io/openems/edge/timeofusetariff/corrently/Corrently.java
new file mode 100644
index 00000000000..ec85c046c34
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.corrently/src/io/openems/edge/timeofusetariff/corrently/Corrently.java
@@ -0,0 +1,26 @@
+package io.openems.edge.timeofusetariff.corrently;
+
+import io.openems.common.types.OpenemsType;
+import io.openems.edge.common.channel.Doc;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.timeofusetariff.api.TimeOfUseTariff;
+
+public interface Corrently extends TimeOfUseTariff, OpenemsComponent {
+
+ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
+ HTTP_STATUS_CODE(Doc.of(OpenemsType.INTEGER)//
+ .text("Displays the HTTP status code"))//
+ ;
+
+ private final Doc doc;
+
+ private ChannelId(Doc doc) {
+ this.doc = doc;
+ }
+
+ @Override
+ public Doc doc() {
+ return this.doc;
+ }
+ }
+}
diff --git a/io.openems.edge.timeofusetariff.corrently/src/io/openems/edge/timeofusetariff/corrently/CorrentlyImpl.java b/io.openems.edge.timeofusetariff.corrently/src/io/openems/edge/timeofusetariff/corrently/CorrentlyImpl.java
new file mode 100644
index 00000000000..ae091ad0d2e
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.corrently/src/io/openems/edge/timeofusetariff/corrently/CorrentlyImpl.java
@@ -0,0 +1,178 @@
+package io.openems.edge.timeofusetariff.corrently;
+
+import java.io.IOException;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.TreeMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+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.metatype.annotations.Designate;
+
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.common.utils.JsonUtils;
+import io.openems.common.utils.ThreadPoolUtils;
+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.timeofusetariff.api.TimeOfUsePrices;
+import io.openems.edge.timeofusetariff.api.TimeOfUseTariff;
+import io.openems.edge.timeofusetariff.api.utils.TimeOfUseTariffUtils;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+@Designate(ocd = Config.class, factory = true)
+@Component(//
+ name = "TimeOfUseTariff.Corrently", //
+ immediate = true, //
+ configurationPolicy = ConfigurationPolicy.REQUIRE //
+)
+public class CorrentlyImpl extends AbstractOpenemsComponent implements TimeOfUseTariff, OpenemsComponent, Corrently {
+
+ private static final String CORRENTLY_API_URL = "https://api.corrently.io/v2.0/gsi/marketdata?zipcode=";
+
+ private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+
+ private Config config = null;
+
+ private final AtomicReference> prices = new AtomicReference>(
+ ImmutableSortedMap.of());
+
+ private Clock updateTimeStamp = Clock.fixed(Instant.MIN, ZoneId.systemDefault());
+
+ private final Runnable task = () -> {
+
+ /*
+ * Update Map of prices
+ */
+ ImmutableSortedMap prices;
+ try {
+ Integer zipcode = this.config.zipcode();
+ OkHttpClient client = new OkHttpClient();
+ Request request = new Request.Builder() //
+ .url(CORRENTLY_API_URL.concat(zipcode.toString())) //
+ // .header("Authorization", Credentials.basic(apikey, "")) //
+ .build();
+
+ Response response = client.newCall(request).execute();
+ this.channel(Corrently.ChannelId.HTTP_STATUS_CODE).setNextValue(response.code());
+
+ if (!response.isSuccessful()) {
+ throw new IOException("Unexpected code " + response);
+ }
+
+ // Parse the response for the prices
+ prices = CorrentlyImpl.parsePrices(response.body().toString());
+
+ // store the time stamp
+ this.updateTimeStamp = Clock.systemDefaultZone();
+
+ } catch (IOException | OpenemsNamedException e) {
+ e.printStackTrace();
+ prices = ImmutableSortedMap.of();
+ }
+
+ this.prices.set(prices);
+
+ /*
+ * Schedule next price update for 2 pm
+ */
+ ZonedDateTime now = ZonedDateTime.now();
+ ZonedDateTime nextRun = now.withHour(14).truncatedTo(ChronoUnit.HOURS);
+ if (now.isAfter(nextRun)) {
+ nextRun = nextRun.plusDays(1);
+ }
+
+ Duration duration = Duration.between(now, nextRun);
+ long delay = duration.getSeconds();
+
+ this.executor.schedule(this.task, delay, TimeUnit.SECONDS);
+ };
+
+ @Reference
+ private ComponentManager componentManager;
+
+ public CorrentlyImpl() {
+ super(//
+ OpenemsComponent.ChannelId.values(), //
+ Corrently.ChannelId.values() //
+ );
+ }
+
+ @Activate
+ void activate(ComponentContext context, Config config) {
+ super.activate(context, config.id(), config.alias(), config.enabled());
+
+ if (!config.enabled()) {
+ return;
+ }
+ this.config = config;
+ this.executor.schedule(this.task, 0, TimeUnit.SECONDS);
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ super.deactivate();
+ ThreadPoolUtils.shutdownAndAwaitTermination(this.executor, 0);
+ }
+
+ @Override
+ public TimeOfUsePrices getPrices() {
+ return TimeOfUseTariffUtils.getNext24HourPrices(Clock.systemDefaultZone() /* can be mocked for testing */,
+ this.prices.get(), this.updateTimeStamp);
+ }
+
+ /**
+ * Parse the Corrently JSON to the Price Map.
+ *
+ * @param jsonData the Corrently JSON
+ * @return the Price Map
+ * @throws OpenemsNamedException on error
+ */
+ public static ImmutableSortedMap parsePrices(String jsonData) throws OpenemsNamedException {
+ TreeMap result = new TreeMap<>();
+
+ if (!jsonData.isEmpty()) {
+
+ JsonObject line = JsonUtils.getAsJsonObject(JsonUtils.parse(jsonData));
+ JsonArray data = JsonUtils.getAsJsonArray(line, "data");
+
+ for (JsonElement element : data) {
+
+ float marketPrice = JsonUtils.getAsFloat(element, "marketprice");
+ long startTimestampLong = JsonUtils.getAsLong(element, "start_timestamp");
+
+ // Converting Long time stamp to ZonedDateTime.
+ ZonedDateTime startTimeStamp = ZonedDateTime //
+ .ofInstant(Instant.ofEpochMilli(startTimestampLong), ZoneId.systemDefault())
+ .truncatedTo(ChronoUnit.HOURS);
+
+ // Adding the values in the Map.
+ result.put(startTimeStamp, marketPrice);
+ result.put(startTimeStamp.plusMinutes(15), marketPrice);
+ result.put(startTimeStamp.plusMinutes(30), marketPrice);
+ result.put(startTimeStamp.plusMinutes(45), marketPrice);
+ }
+ }
+ return ImmutableSortedMap.copyOf(result);
+ }
+
+}
diff --git a/io.openems.edge.timeofusetariff.corrently/test/.gitignore b/io.openems.edge.timeofusetariff.corrently/test/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/io.openems.edge.timeofusetariff.corrently/test/io/openems/edge/timeofusetariff/corrently/CorrentlyProviderTest.java b/io.openems.edge.timeofusetariff.corrently/test/io/openems/edge/timeofusetariff/corrently/CorrentlyProviderTest.java
new file mode 100644
index 00000000000..738283bbd3b
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.corrently/test/io/openems/edge/timeofusetariff/corrently/CorrentlyProviderTest.java
@@ -0,0 +1,98 @@
+package io.openems.edge.timeofusetariff.corrently;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.time.ZonedDateTime;
+import java.util.SortedMap;
+
+import org.junit.Test;
+
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.edge.common.test.ComponentTest;
+
+public class CorrentlyProviderTest {
+
+ private static final String CTRL_ID = "ctrl0";
+
+ @Test
+ public void test() throws Exception {
+ new ComponentTest(new CorrentlyImpl()) //
+ .activate(MyConfig.create() //
+ .setId(CTRL_ID) //
+ .setZipcode(94469 /* Deggendorf, Germany */) //
+ .build()) //
+ ;
+ }
+
+ @Test
+ public void nonEmptyStringTest() throws OpenemsNamedException {
+ // Parsing with custom data
+ SortedMap prices = CorrentlyImpl.parsePrices("{" + " \"object\": \"list\","
+ + " \"data\": [" + " {" + " \"start_timestamp\": 1632402000000,"
+ + " \"end_timestamp\": 1632405600000," + " \"marketprice\": 158.95,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632405600000,"
+ + " \"end_timestamp\": 1632409200000," + " \"marketprice\": 160.98,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632409200000,"
+ + " \"end_timestamp\": 1632412800000," + " \"marketprice\": 171.15,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632412800000,"
+ + " \"end_timestamp\": 1632416400000," + " \"marketprice\": 174.96,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632416400000,"
+ + " \"end_timestamp\": 1632420000000," + " \"marketprice\": 161.53,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632420000000,"
+ + " \"end_timestamp\": 1632423600000," + " \"marketprice\": 152,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632423600000,"
+ + " \"end_timestamp\": 1632427200000," + " \"marketprice\": 120.01,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632427200000,"
+ + " \"end_timestamp\": 1632430800000," + " \"marketprice\": 111.03,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632430800000,"
+ + " \"end_timestamp\": 1632434400000," + " \"marketprice\": 105.04,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632434400000,"
+ + " \"end_timestamp\": 1632438000000," + " \"marketprice\": 105,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632438000000,"
+ + " \"end_timestamp\": 1632441600000," + " \"marketprice\": 74.23,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632441600000,"
+ + " \"end_timestamp\": 1632445200000," + " \"marketprice\": 73.28,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632445200000,"
+ + " \"end_timestamp\": 1632448800000," + " \"marketprice\": 67.97,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632448800000,"
+ + " \"end_timestamp\": 1632452400000," + " \"marketprice\": 72.53,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632452400000,"
+ + " \"end_timestamp\": 1632456000000," + " \"marketprice\": 89.66,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632456000000,"
+ + " \"end_timestamp\": 1632459600000," + " \"marketprice\": 150.1,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632459600000,"
+ + " \"end_timestamp\": 1632463200000," + " \"marketprice\": 173.54,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632463200000,"
+ + " \"end_timestamp\": 1632466800000," + " \"marketprice\": 178.4,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632466800000,"
+ + " \"end_timestamp\": 1632470400000," + " \"marketprice\": 158.91,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632470400000,"
+ + " \"end_timestamp\": 1632474000000," + " \"marketprice\": 140.01,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632474000000,"
+ + " \"end_timestamp\": 1632477600000," + " \"marketprice\": 149.99,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632477600000,"
+ + " \"end_timestamp\": 1632481200000," + " \"marketprice\": 157.43,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632481200000,"
+ + " \"end_timestamp\": 1632484800000," + " \"marketprice\": 130.9,"
+ + " \"unit\": \"Eur/MWh\"" + " }," + " {" + " \"start_timestamp\": 1632484800000,"
+ + " \"end_timestamp\": 1632488400000," + " \"marketprice\": 120.14,"
+ + " \"unit\": \"Eur/MWh\"" + " }" + " ]," + " \"url\": \"/at/v1/marketdata\"" + "}"); //
+
+ // To check if the Map is not empty
+ assertFalse(prices.isEmpty());
+
+ // To check if the a value input from the string is present in map.
+ assertTrue(prices.containsValue(120.14f));
+
+ }
+
+ @Test
+ public void emptyStringTest() throws OpenemsNamedException {
+ // Parsing with empty string
+ SortedMap prices = CorrentlyImpl.parsePrices("");
+
+ // To check if the map is empty.
+ assertTrue(prices.isEmpty());
+ }
+}
diff --git a/io.openems.edge.timeofusetariff.corrently/test/io/openems/edge/timeofusetariff/corrently/MyConfig.java b/io.openems.edge.timeofusetariff.corrently/test/io/openems/edge/timeofusetariff/corrently/MyConfig.java
new file mode 100644
index 00000000000..280be7d9b49
--- /dev/null
+++ b/io.openems.edge.timeofusetariff.corrently/test/io/openems/edge/timeofusetariff/corrently/MyConfig.java
@@ -0,0 +1,51 @@
+package io.openems.edge.timeofusetariff.corrently;
+
+import io.openems.edge.common.test.AbstractComponentConfig;
+
+@SuppressWarnings("all")
+public class MyConfig extends AbstractComponentConfig implements Config {
+
+ public static class Builder {
+ private String id;
+ public int zipcode;
+
+ private Builder() {
+ }
+
+ public Builder setId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setZipcode(int zipcode) {
+ this.zipcode = zipcode;
+ 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 int zipcode() {
+ return this.builder.zipcode;
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.shared.influxdb/src/io/openems/shared/influxdb/InfluxConnector.java b/io.openems.shared.influxdb/src/io/openems/shared/influxdb/InfluxConnector.java
index 735f82e8e77..f8fcc7b65db 100644
--- a/io.openems.shared.influxdb/src/io/openems/shared/influxdb/InfluxConnector.java
+++ b/io.openems.shared.influxdb/src/io/openems/shared/influxdb/InfluxConnector.java
@@ -34,6 +34,7 @@
import com.google.gson.JsonNull;
import com.google.gson.JsonPrimitive;
+import io.openems.common.OpenemsOEM;
import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.exceptions.OpenemsException;
import io.openems.common.types.ChannelAddress;
@@ -244,7 +245,7 @@ public SortedMap queryHistoricEnergy(Optional ");
b.append(String.valueOf(fromDate.toEpochSecond()));
@@ -293,7 +294,7 @@ public SortedMap> queryHis
b.append(InfluxConnector.toChannelAddressStringNonNegativeDifferenceLast(channels));
b.append(" FROM data WHERE ");
if (influxEdgeId.isPresent()) {
- b.append(InfluxConstants.TAG + " = '" + influxEdgeId.get() + "' AND ");
+ b.append(OpenemsOEM.INFLUXDB_TAG + " = '" + influxEdgeId.get() + "' AND ");
}
b.append("time > ");
b.append(String.valueOf(fromDate.toEpochSecond()));
@@ -335,7 +336,7 @@ public SortedMap> queryHis
query.append(InfluxConnector.toChannelAddressStringData(channels));
query.append(" FROM data WHERE ");
if (influxEdgeId.isPresent()) {
- query.append(InfluxConstants.TAG + " = '" + influxEdgeId.get() + "' AND ");
+ query.append(OpenemsOEM.INFLUXDB_TAG + " = '" + influxEdgeId.get() + "' AND ");
}
query.append("time > ");
query.append(String.valueOf(fromDate.toEpochSecond()));
diff --git a/io.openems.shared.influxdb/src/io/openems/shared/influxdb/InfluxConstants.java b/io.openems.shared.influxdb/src/io/openems/shared/influxdb/InfluxConstants.java
deleted file mode 100644
index 8a4f290c24f..00000000000
--- a/io.openems.shared.influxdb/src/io/openems/shared/influxdb/InfluxConstants.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package io.openems.shared.influxdb;
-
-public class InfluxConstants {
-
- public static final String TAG = "edge";
-
-}
diff --git a/ui/README.md b/ui/README.md
index 2e1a2108cb0..ff26b922296 100644
--- a/ui/README.md
+++ b/ui/README.md
@@ -20,7 +20,7 @@ This project was generated with [angular-cli](https://github.com/angular/angular
- Build Production
- `ng build -c "openems,openems-edge-dev,prod"`
+ `ng build -c "openems,openems-edge-prod,prod"`
- OpenEMS Backend - expects a Backend *Ui.Websocket* on default port `8082`
@@ -80,4 +80,4 @@ ngOnDestroy() {
this.stopOnDestroy.next();
this.stopOnDestroy.complete();
}
-```
\ No newline at end of file
+```
diff --git a/ui/angular.json b/ui/angular.json
index 943c94e2618..48e1bb581a4 100644
--- a/ui/angular.json
+++ b/ui/angular.json
@@ -153,4 +153,4 @@
"styleext": "scss"
}
}
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts
index a9136ae3314..7dc96c1dc6d 100644
--- a/ui/src/app/app-routing.module.ts
+++ b/ui/src/app/app-routing.module.ts
@@ -17,6 +17,7 @@ import { ProductionChartOverviewComponent } from './edge/history/production/prod
import { SelfconsumptionChartOverviewComponent } from './edge/history/selfconsumption/selfconsumptionchartoverview/selfconsumptionchartoverview.component';
import { SinglethresholdChartOverviewComponent } from './edge/history/singlethreshold/singlethresholdchartoverview/singlethresholdchartoverview.component';
import { StorageChartOverviewComponent } from './edge/history/storage/storagechartoverview/storagechartoverview.component';
+import { TimeOfUseTariffDischargeChartOverviewComponent } from './edge/history/timeofusetariffdischarge/timeofusetariffdischargeoverview/timeofusetariffdischargechartoverview.component';
import { LiveComponent as EdgeLiveComponent } from './edge/live/live.component';
import { ChannelsComponent as EdgeSettingsChannelsComponent } from './edge/settings/channels/channels.component';
import { IndexComponent as EdgeSettingsComponentInstallIndexComponentComponent } from './edge/settings/component/install/index.component';
@@ -29,6 +30,7 @@ import { ProfileComponent as EdgeSettingsProfileComponent } from './edge/setting
import { SettingsComponent as EdgeSettingsComponent } from './edge/settings/settings.component';
import { SystemExecuteComponent as EdgeSettingsSystemExecuteComponent } from './edge/settings/systemexecute/systemexecute.component';
import { SystemLogComponent as EdgeSettingsSystemLogComponent } from './edge/settings/systemlog/systemlog.component';
+import { SystemUpdateComponent as EdgeSettingsSystemUpdateComponent } from './edge/settings/systemupdate/systemupdate.component';
import { IndexComponent } from './index/index.component';
import { UserComponent } from './user/user.component';
@@ -54,6 +56,7 @@ const routes: Routes = [
{ path: 'device/:edgeId/history/:componentId/singlethresholdchart', component: SinglethresholdChartOverviewComponent },
{ path: 'device/:edgeId/history/:componentId/symmetricpeakshavingchart', component: SymmetricPeakshavingChartOverviewComponent },
{ path: 'device/:edgeId/history/:componentId/timeslotpeakshavingchart', component: TimeslotPeakshavingChartOverviewComponent },
+ { path: 'device/:edgeId/history/:componentId/timeOfUseTariffDischargeChart', component: TimeOfUseTariffDischargeChartOverviewComponent },
{ path: 'device/:edgeId/history/autarchychart', component: AutarchyChartOverviewComponent },
{ path: 'device/:edgeId/history/consumptionchart', component: ConsumptionChartOverviewComponent },
{ path: 'device/:edgeId/history/gridchart', component: GridChartOverviewComponent },
@@ -72,6 +75,7 @@ const routes: Routes = [
{ path: 'device/:edgeId/settings/profile/:componentId', component: AliasUpdateComponent },
{ path: 'device/:edgeId/settings/systemexecute', component: EdgeSettingsSystemExecuteComponent },
{ path: 'device/:edgeId/settings/systemlog', component: EdgeSettingsSystemLogComponent },
+ { path: 'device/:edgeId/settings/systemupdate', component: EdgeSettingsSystemUpdateComponent },
];
export const appRoutingProviders: any[] = [
diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts
index 014664d3999..7b51073464d 100644
--- a/ui/src/app/app.component.ts
+++ b/ui/src/app/app.component.ts
@@ -6,6 +6,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { environment } from '../environments';
import { Service, Websocket } from './shared/shared';
+import { LanguageTag } from './shared/translate/language';
@Component({
selector: 'app-root',
@@ -29,7 +30,7 @@ export class AppComponent {
public websocket: Websocket,
private titleService: Title
) {
- service.setLang(this.service.browserLangToLangTag(navigator.language));
+ service.setLang(LanguageTag[localStorage.LANGUAGE] ?? this.service.browserLangToLangTag(navigator.language));
}
ngOnInit() {
diff --git a/ui/src/app/edge/history/abstracthistorychart.ts b/ui/src/app/edge/history/abstracthistorychart.ts
index f9e0b831b79..eecdee894a7 100644
--- a/ui/src/app/edge/history/abstracthistorychart.ts
+++ b/ui/src/app/edge/history/abstracthistorychart.ts
@@ -69,11 +69,15 @@ export abstract class AbstractHistoryChart {
* @param edge the current Edge
* @param ws the websocket
*/
- protected queryHistoricTimeseriesData(fromDate: Date, toDate: Date): Promise {
+ protected queryHistoricTimeseriesData(fromDate: Date, toDate: Date, resolution?: number): Promise {
// TODO should be removed, edge delivers too much data
let newDate = (this.service.periodString == 'year' ? addMonths(fromDate, 1) : this.service.periodString == 'month' ? addDays(fromDate, 1) : fromDate);
- let resolution = calculateResolution(this.service, fromDate, toDate);
+
+ if (resolution == null) {
+ resolution = calculateResolution(this.service, fromDate, toDate);
+ }
+
return new Promise((resolve, reject) => {
this.service.getCurrentEdge().then(edge => {
this.service.getConfig().then(config => {
@@ -105,11 +109,12 @@ export abstract class AbstractHistoryChart {
// TODO should be removed, edge delivers too much data
let newDate = this.service.periodString == 'year' ? addMonths(fromDate, 1) : addDays(fromDate, 1)
+
let resolution = calculateResolution(this.service, fromDate, toDate);
+
return new Promise((resolve, reject) => {
this.service.getCurrentEdge().then(edge => {
this.service.getConfig().then(config => {
-
edge.sendRequest(this.service.websocket, new queryHistoricTimeseriesEnergyPerPeriodRequest(newDate, toDate, channelAddresses, resolution)).then(response => {
let result = (response as QueryHistoricTimeseriesDataResponse).result;
diff --git a/ui/src/app/edge/history/autarchy/chart.component.ts b/ui/src/app/edge/history/autarchy/chart.component.ts
index 54ed289f955..8fc25a50d1e 100644
--- a/ui/src/app/edge/history/autarchy/chart.component.ts
+++ b/ui/src/app/edge/history/autarchy/chart.component.ts
@@ -98,7 +98,6 @@ export class AutarchyChartComponent extends AbstractHistoryChart implements OnIn
return CurrentData.calculateAutarchy(buyFromGridData[index], value);
}
})
-
datasets.push({
label: this.translate.instant('General.autarchy'),
data: autarchy,
diff --git a/ui/src/app/edge/history/consumption/otherchart.component.ts b/ui/src/app/edge/history/consumption/otherchart.component.ts
index 8795ed597b4..ffaacb5daf0 100644
--- a/ui/src/app/edge/history/consumption/otherchart.component.ts
+++ b/ui/src/app/edge/history/consumption/otherchart.component.ts
@@ -65,12 +65,12 @@ export class ConsumptionOtherChartComponent extends AbstractHistoryChart impleme
});
})
- let totalMetersConsumption: number[] = [];
+ let totalMeteredConsumption: number[] = [];
config.getComponentsImplementingNature("io.openems.edge.meter.api.SymmetricMeter")
.filter(component => component.isEnabled && config.isTypeConsumptionMetered(component))
.forEach(component => {
- totalMetersConsumption = result.data[component.id + '/ActivePower'].map((value, index) => {
- return Utils.addSafely(totalMetersConsumption[index], value / 1000)
+ totalMeteredConsumption = result.data[component.id + '/ActivePower'].map((value, index) => {
+ return Utils.addSafely(totalMeteredConsumption[index], value / 1000)
})
})
@@ -80,13 +80,13 @@ export class ConsumptionOtherChartComponent extends AbstractHistoryChart impleme
if (value != null) {
- // Check if either totalEvcsConsumption or totalMetersConsumption is not null
- return Utils.subtractSafely(Utils.subtractSafely(value / 1000, totalEvcsConsumption[index]), totalMetersConsumption[index]);
+ // Check if either totalEvcsConsumption or totalMeteredConsumption is not null
+ return Utils.subtractSafely(Utils.subtractSafely(value / 1000, totalEvcsConsumption[index]), totalMeteredConsumption[index]);
}
})
// show other consumption if at least one of the arrays is not empty
- if (totalEvcsConsumption != [] || totalMetersConsumption != []) {
+ if (totalEvcsConsumption != [] || totalMeteredConsumption != []) {
datasets.push({
label: this.translate.instant('General.consumption'),
data: otherConsumption,
@@ -144,4 +144,4 @@ export class ConsumptionOtherChartComponent extends AbstractHistoryChart impleme
public getChartHeight(): number {
return window.innerHeight / 21 * 9;
}
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/edge/history/consumption/totalchart.component.ts b/ui/src/app/edge/history/consumption/totalchart.component.ts
index 0bf0d1f258b..3ff9f1074b8 100644
--- a/ui/src/app/edge/history/consumption/totalchart.component.ts
+++ b/ui/src/app/edge/history/consumption/totalchart.component.ts
@@ -87,20 +87,14 @@ export class ConsumptionTotalChartComponent extends AbstractHistoryChart impleme
// gather other Consumption (Total - EVCS - consumptionMetered)
let otherConsumption: number[] = [];
- if (totalEvcsConsumption != []) {
- otherConsumption = result.data['_sum/ConsumptionActivePower'].map((value, index) => {
- if (value != null && totalEvcsConsumption[index] != null) {
- return Utils.subtractSafely(value / 1000, totalEvcsConsumption[index]);
- }
- })
- }
- if (totalMeteredConsumption != []) {
- otherConsumption = result.data['_sum/ConsumptionActivePower'].map((value, index) => {
- if (value != null && totalMeteredConsumption[index] != null) {
- return Utils.subtractSafely(value / 1000, totalMeteredConsumption[index]);
- }
- })
- }
+ otherConsumption = result.data['_sum/ConsumptionActivePower'].map((value, index) => {
+
+ if (value != null) {
+
+ // Check if either totalEvcsConsumption or totalMeteredConsumption is not null
+ return Utils.subtractSafely(Utils.subtractSafely(value / 1000, totalEvcsConsumption[index]), totalMeteredConsumption[index]);
+ }
+ })
// convert datasets
let datasets = [];
diff --git a/ui/src/app/edge/history/history.component.html b/ui/src/app/edge/history/history.component.html
index 1f61d5f3d0e..e41956425fd 100644
--- a/ui/src/app/edge/history/history.component.html
+++ b/ui/src/app/edge/history/history.component.html
@@ -20,10 +20,10 @@
-
+
-
+
@@ -61,6 +61,13 @@
+
+
+
+
+
+
diff --git a/ui/src/app/edge/history/history.module.ts b/ui/src/app/edge/history/history.module.ts
index dbcc2445a00..23cf27e2b97 100644
--- a/ui/src/app/edge/history/history.module.ts
+++ b/ui/src/app/edge/history/history.module.ts
@@ -1,8 +1,7 @@
-import { AsymmetricPeakshavingChartComponent } from './peakshaving/asymmetric/chart.component';
-import { AsymmetricPeakshavingChartOverviewComponent } from './peakshaving/asymmetric/asymmetricpeakshavingchartoverview/asymmetricpeakshavingchartoverview.component';
-import { AsymmetricPeakshavingWidgetComponent } from './peakshaving/asymmetric/widget.component';
-import { AutarchyChartComponent } from './autarchy/chart.component';
+import { NgModule } from '@angular/core';
+import { SharedModule } from '../../shared/shared.module';
import { AutarchyChartOverviewComponent } from './autarchy/autarchychartoverview/autarchychartoverview.component';
+import { AutarchyChartComponent } from './autarchy/chart.component';
import { AutarchyWidgetComponent } from './autarchy/widget.component';
import { ChannelthresholdChartOverviewComponent } from './channelthreshold/channelthresholdchartoverview/channelthresholdchartoverview.component';
import { ChannelthresholdSingleChartComponent } from './channelthreshold/singlechart.component';
@@ -11,12 +10,12 @@ import { ChannelthresholdWidgetComponent } from './channelthreshold/widget.compo
import { ChpSocChartComponent } from './chpsoc/chart.component';
import { ChpSocWidgetComponent } from './chpsoc/widget.component';
import { ConsumptionChartOverviewComponent } from './consumption/consumptionchartoverview/consumptionchartoverview.component';
-import { ConsumptionComponent } from './consumption/widget.component';
import { ConsumptionEvcsChartComponent } from './consumption/evcschart.component';
import { ConsumptionMeterChartComponent } from './consumption/meterchart.component';
import { ConsumptionOtherChartComponent } from './consumption/otherchart.component';
import { ConsumptionSingleChartComponent } from './consumption/singlechart.component';
import { ConsumptionTotalChartComponent } from './consumption/totalchart.component';
+import { ConsumptionComponent } from './consumption/widget.component';
import { DelayedSellToGridChartComponent } from './delayedselltogrid/chart.component';
import { DelayedSellToGridChartOverviewComponent } from './delayedselltogrid/symmetricpeakshavingchartoverview/delayedselltogridchartoverview.component';
import { DelayedSellToGridWidgetComponent } from './delayedselltogrid/widget.component';
@@ -29,9 +28,10 @@ import { FixDigitalOutputWidgetComponent } from './fixdigitaloutput/widget.compo
import { GridChartComponent } from './grid/chart.component';
import { GridChartOverviewComponent } from './grid/gridchartoverview/gridchartoverview.component';
import { GridComponent } from './grid/widget.component';
-import { GridOptimizedChargeWidgetComponent } from './gridoptimizedcharge/widget.component';
import { GridOptimizedChargeChartComponent } from './gridoptimizedcharge/chart.component';
import { GridOptimizedChargeChartOverviewComponent } from './gridoptimizedcharge/gridoptimizedchargechartoverview/gridoptimizedchargechartoverview.component';
+import { SellToGridLimitChartComponent } from './gridoptimizedcharge/sellToGridLimitChart.component';
+import { GridOptimizedChargeWidgetComponent } from './gridoptimizedcharge/widget.component';
import { HeatingelementChartComponent } from './heatingelement/chart.component';
import { HeatingelementChartOverviewComponent } from './heatingelement/heatingelementchartoverview/heatingelementchartoverview.component';
import { HeatingelementWidgetComponent } from './heatingelement/widget.component';
@@ -39,36 +39,39 @@ import { HeatPumpChartComponent } from './heatpump/chart.component';
import { HeatPumpChartOverviewComponent } from './heatpump/heatpumpchartoverview/heatpumpchartoverview.component';
import { HeatpumpWidgetComponent } from './heatpump/widget.component';
import { HistoryComponent } from './history.component';
-import { NgModule } from '@angular/core';
+import { AsymmetricPeakshavingChartOverviewComponent } from './peakshaving/asymmetric/asymmetricpeakshavingchartoverview/asymmetricpeakshavingchartoverview.component';
+import { AsymmetricPeakshavingChartComponent } from './peakshaving/asymmetric/chart.component';
+import { AsymmetricPeakshavingWidgetComponent } from './peakshaving/asymmetric/widget.component';
+import { SymmetricPeakshavingChartComponent } from './peakshaving/symmetric/chart.component';
+import { SymmetricPeakshavingChartOverviewComponent } from './peakshaving/symmetric/symmetricpeakshavingchartoverview/symmetricpeakshavingchartoverview.component';
+import { SymmetricPeakshavingWidgetComponent } from './peakshaving/symmetric/widget.component';
+import { TimeslotPeakshavingChartComponent } from './peakshaving/timeslot/chart.component';
+import { TimeslotPeakshavingChartOverviewComponent } from './peakshaving/timeslot/timeslotpeakshavingchartoverview/timeslotpeakshavingchartoverview.component';
+import { TimeslotPeakshavingWidgetComponent } from './peakshaving/timeslot/widget.component';
import { ProductionChargerChartComponent } from './production/chargerchart.component';
import { ProductionChartOverviewComponent } from './production/productionchartoverview/productionchartoverview.component';
-import { ProductionComponent } from './production/widget.component';
import { ProductionMeterChartComponent } from './production/productionmeterchart';
import { ProductionSingleChartComponent } from './production/singlechart';
import { ProductionTotalAcChartComponent } from './production/totalacchart';
import { ProductionTotalChartComponent } from './production/totalchart';
import { ProductionTotalDcChartComponent } from './production/totaldcchart';
+import { ProductionComponent } from './production/widget.component';
import { SelfconsumptionChartComponent } from './selfconsumption/chart.component';
import { SelfconsumptionChartOverviewComponent } from './selfconsumption/selfconsumptionchartoverview/selfconsumptionchartoverview.component';
import { SelfconsumptionWidgetComponent } from './selfconsumption/widget.component';
-import { SellToGridLimitChartComponent } from './gridoptimizedcharge/sellToGridLimitChart.component';
-import { SharedModule } from '../../shared/shared.module';
import { SinglethresholdChartComponent } from './singlethreshold/chart.component';
import { SinglethresholdChartOverviewComponent } from './singlethreshold/singlethresholdchartoverview/singlethresholdchartoverview.component';
import { SinglethresholdWidgetComponent } from './singlethreshold/widget.component';
-import { SocStorageChartComponent } from './storage/socchart.component';
import { StorageChargerChartComponent } from './storage/chargerchart.component';
-import { StorageChartOverviewComponent } from './storage/storagechartoverview/storagechartoverview.component';
-import { StorageComponent } from './storage/widget.component';
import { StorageESSChartComponent } from './storage/esschart.component';
import { StorageSingleChartComponent } from './storage/singlechart.component';
+import { SocStorageChartComponent } from './storage/socchart.component';
+import { StorageChartOverviewComponent } from './storage/storagechartoverview/storagechartoverview.component';
import { StorageTotalChartComponent } from './storage/totalchart.component';
-import { SymmetricPeakshavingChartComponent } from './peakshaving/symmetric/chart.component';
-import { SymmetricPeakshavingChartOverviewComponent } from './peakshaving/symmetric/symmetricpeakshavingchartoverview/symmetricpeakshavingchartoverview.component';
-import { SymmetricPeakshavingWidgetComponent } from './peakshaving/symmetric/widget.component';
-import { TimeslotPeakshavingChartComponent } from './peakshaving/timeslot/chart.component';
-import { TimeslotPeakshavingChartOverviewComponent } from './peakshaving/timeslot/timeslotpeakshavingchartoverview/timeslotpeakshavingchartoverview.component';
-import { TimeslotPeakshavingWidgetComponent } from './peakshaving/timeslot/widget.component';
+import { StorageComponent } from './storage/widget.component';
+import { TimeOfUseTariffDischargeChartComponent } from './timeofusetariffdischarge/chart.component';
+import { TimeOfUseTariffDischargeChartOverviewComponent } from './timeofusetariffdischarge/timeofusetariffdischargeoverview/timeofusetariffdischargechartoverview.component';
+import { TimeOfUseTariffDischargeWidgetComponent } from './timeofusetariffdischarge/widget.component';
@NgModule({
imports: [
@@ -144,6 +147,9 @@ import { TimeslotPeakshavingWidgetComponent } from './peakshaving/timeslot/widge
SymmetricPeakshavingChartComponent,
SymmetricPeakshavingChartOverviewComponent,
SymmetricPeakshavingWidgetComponent,
+ TimeOfUseTariffDischargeChartComponent,
+ TimeOfUseTariffDischargeChartOverviewComponent,
+ TimeOfUseTariffDischargeWidgetComponent,
TimeslotPeakshavingChartComponent,
TimeslotPeakshavingChartOverviewComponent,
TimeslotPeakshavingWidgetComponent,
diff --git a/ui/src/app/edge/history/storage/socchart.component.ts b/ui/src/app/edge/history/storage/socchart.component.ts
index e12293dc134..8d68ac3317f 100644
--- a/ui/src/app/edge/history/storage/socchart.component.ts
+++ b/ui/src/app/edge/history/storage/socchart.component.ts
@@ -14,6 +14,7 @@ import { TranslateService } from '@ngx-translate/core';
export class SocStorageChartComponent extends AbstractHistoryChart implements OnInit, OnChanges {
@Input() public period: DefaultTypes.HistoryPeriod;
+ private emergencyCapacityReserveComponents: EdgeConfig.Component[] = [];
ngOnChanges() {
this.updateChart();
@@ -95,11 +96,15 @@ export class SocStorageChartComponent extends AbstractHistoryChart implements On
})
}
}
- if (channelAddress.channelId == '_PropertyReserveSoc') {
+ if (channelAddress.channelId == '_PropertyReserveSoc' &&
+ this.emergencyCapacityReserveComponents.find(
+ element => element.id == channelAddress.componentId)
+ .properties.isReserveSocEnabled) {
datasets.push({
- label: component.alias,
+ label:
+ this.emergencyCapacityReserveComponents.length > 1 ? component.alias : this.translate.instant("Edge.Index.EmergencyReserve.emergencyReserve"),
data: data,
- borderDash: [3, 3]
+ borderDash: [3, 3],
})
this.colors.push({
backgroundColor: 'rgba(1, 1, 1,0)',
@@ -133,8 +138,11 @@ export class SocStorageChartComponent extends AbstractHistoryChart implements On
let channeladdresses: ChannelAddress[] = [];
channeladdresses.push(new ChannelAddress('_sum', 'EssSoc'));
- config.getComponentsImplementingNature('io.openems.edge.controller.ess.emergencycapacityreserve.EmergencyCapacityReserve')
+
+ this.emergencyCapacityReserveComponents = config.getComponentsByFactory('Controller.Ess.EmergencyCapacityReserve')
.filter(component => component.isEnabled)
+
+ this.emergencyCapacityReserveComponents
.forEach(component =>
channeladdresses.push(new ChannelAddress(component.id, '_PropertyReserveSoc'))
);
@@ -164,4 +172,4 @@ export class SocStorageChartComponent extends AbstractHistoryChart implements On
public getChartHeight(): number {
return window.innerHeight / 21 * 9;
}
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/edge/history/timeofusetariffdischarge/chart.component.ts b/ui/src/app/edge/history/timeofusetariffdischarge/chart.component.ts
new file mode 100644
index 00000000000..12d1cd7a5b6
--- /dev/null
+++ b/ui/src/app/edge/history/timeofusetariffdischarge/chart.component.ts
@@ -0,0 +1,318 @@
+import { formatNumber } from '@angular/common';
+import { Component, Input, OnChanges, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { TranslateService } from '@ngx-translate/core';
+import { differenceInDays } from 'date-fns';
+import { DefaultTypes } from 'src/app/shared/service/defaulttypes';
+import { QueryHistoricTimeseriesDataResponse } from '../../../shared/jsonrpc/response/queryHistoricTimeseriesDataResponse';
+import { ChannelAddress, Edge, EdgeConfig, Service } from '../../../shared/shared';
+import { AbstractHistoryChart } from '../abstracthistorychart';
+import { Data, TooltipItem } from '../shared';
+
+@Component({
+ selector: 'timeOfUseTariffDischargeChart',
+ templateUrl: '../abstracthistorychart.html'
+})
+export class TimeOfUseTariffDischargeChartComponent extends AbstractHistoryChart implements OnInit, OnChanges {
+
+ @Input() public period: DefaultTypes.HistoryPeriod;
+ @Input() public componentId: string;
+
+ ngOnChanges() {
+ this.updateChart();
+ };
+
+ constructor(
+ protected service: Service,
+ protected translate: TranslateService,
+ private route: ActivatedRoute,
+ ) {
+ super(service, translate);
+ }
+
+ ngOnInit() {
+ this.spinnerId = "timeOfUseTariffDischarge-chart";
+ this.service.startSpinner(this.spinnerId);
+ this.service.setCurrentComponent('', this.route);
+ }
+
+ ngOnDestroy() {
+ this.unsubscribeChartRefresh()
+ }
+
+ protected updateChart() {
+ this.autoSubscribeChartRefresh();
+ this.service.startSpinner(this.spinnerId);
+ this.colors = [];
+ this.loading = true;
+
+ this.queryHistoricTimeseriesData(this.period.from, this.period.to, 900).then(response => {
+ this.service.getConfig().then(config => {
+ let result = (response as QueryHistoricTimeseriesDataResponse).result;
+
+ // convert labels
+ let labels: Date[] = [];
+ for (let timestamp of result.timestamps) {
+ // Only use full hours as a timestamp
+ labels.push(new Date(timestamp));
+ }
+ this.labels = labels;
+
+ // convert datasets
+ let datasets = [];
+ let quarterlyPrices = this.componentId + '/QuarterlyPrices';
+ let TimeOfUseTariffState = this.componentId + '/StateMachine';
+ // let predictedSocWithoutLogic = this.componentId + '/PredictedSocWithoutLogic';
+
+ if (TimeOfUseTariffState in result.data && quarterlyPrices in result.data) {
+
+ // Get only the 15 minute value
+ let quarterlyPricesStandbyModeData = [];
+ let quarterlyPricesNightData = [];
+ let quarterlyPricesDelayedDischargeData = [];
+ // let predictedSocWithoutLogicData = [];
+
+ for (let i = 0; i < 96; i++) {
+ let quarterlyPrice = this.formatPrice(result.data[quarterlyPrices][i]);
+ let state = result.data[TimeOfUseTariffState][i];
+
+ if (state == null) {
+ quarterlyPricesDelayedDischargeData.push(null);
+ quarterlyPricesNightData.push(null);
+ quarterlyPricesStandbyModeData.push(null);
+ } else {
+ switch (state) {
+ case 0:
+ // delayed
+ quarterlyPricesDelayedDischargeData.push(quarterlyPrice);
+ quarterlyPricesNightData.push(null);
+ quarterlyPricesStandbyModeData.push(null);
+ break;
+ case 1:
+ // allowsDischarge
+ quarterlyPricesDelayedDischargeData.push(null);
+ quarterlyPricesNightData.push(quarterlyPrice)
+ quarterlyPricesStandbyModeData.push(null);
+ break;
+ case -1:
+ // notStarted
+ case 2:
+ // standby
+ quarterlyPricesDelayedDischargeData.push(null);
+ quarterlyPricesNightData.push(null);
+ quarterlyPricesStandbyModeData.push(quarterlyPrice);
+ break;
+ }
+ }
+ }
+
+
+ // Set dataset for no limit
+ datasets.push({
+ type: 'bar',
+ label: this.translate.instant('Edge.Index.Energymonitor.storageDischarge'),
+ data: quarterlyPricesNightData,
+ order: 3,
+ });
+ this.colors.push({
+ // Dark Green
+ backgroundColor: 'rgba(51,102,0,0.8)',
+ borderColor: 'rgba(51,102,0,1)',
+ })
+
+ // Set dataset for buy from grid
+ datasets.push({
+ type: 'bar',
+ label: this.translate.instant('General.gridBuy'),
+ data: quarterlyPricesDelayedDischargeData,
+ order: 4,
+ });
+ this.colors.push({
+ // Black
+ backgroundColor: 'rgba(0,0,0,0.8)',
+ borderColor: 'rgba(0,0,0,0.9)',
+
+ })
+
+ // Set dataset for Quarterly Prices outside zone
+ datasets.push({
+ type: 'bar',
+ label: this.translate.instant('Edge.Index.Widgets.TimeOfUseTariff.State.standby'),
+ data: quarterlyPricesStandbyModeData,
+ order: 3,
+ });
+ this.colors.push({
+ // Dark Blue
+ backgroundColor: 'rgba(0,0,200,0.7)',
+ borderColor: 'rgba(0,0,200,0.9)',
+ })
+
+ // Predicted SoC is not shown for now, because it is not inteligent enough with the simple prediction
+ // if (predictedSocWithoutLogic in result.data) {
+ // for (let i = 0; i < 96; i++) {
+ // let predictedSoc = result.data[predictedSocWithoutLogic][i];
+ // predictedSocWithoutLogicData.push(predictedSoc);
+ // }
+ // }
+
+ // let predictedSocLabel = "Predicted Soc without logic";
+ // datasets.push({
+ // type: 'line',
+ // label: predictedSocLabel,
+ // data: predictedSocWithoutLogicData,
+ // hidden: false,
+ // yAxisID: 'yAxis2',
+ // position: 'right',
+ // borderDash: [10, 10],
+ // order: 2,
+ // });
+ // this.colors.push({
+ // backgroundColor: 'rgba(255,0,0,0.01)',
+ // borderColor: 'rgba(255,0,0,1)'
+ // })
+ }
+
+ // State of charge data
+ if ('_sum/EssSoc' in result.data) {
+ let socData = result.data['_sum/EssSoc'].map(value => {
+ if (value == null) {
+ return null
+ } else if (value > 100 || value < 0) {
+ return null;
+ } else {
+ return value;
+ }
+ })
+ datasets.push({
+ type: 'line',
+ label: this.translate.instant('General.soc'),
+ data: socData,
+ hidden: false,
+ yAxisID: 'yAxis2',
+ position: 'right',
+ borderDash: [10, 10],
+ order: 1,
+ })
+ this.colors.push({
+ backgroundColor: 'rgba(189, 195, 199,0.2)',
+ borderColor: 'rgba(189, 195, 199,1)',
+ })
+ }
+
+ this.datasets = datasets;
+ this.loading = false;
+ this.service.stopSpinner(this.spinnerId);
+ }).catch(reason => {
+ console.error(reason); // TODO error message
+ this.initializeChart();
+ return;
+ });
+ }).catch(reason => {
+ console.error(reason); // TODO error message
+ this.initializeChart();
+ return;
+ });
+ }
+
+ /**
+ * Converts a value in €/MWh to €/kWh.
+ *
+ * @param price the price value
+ * @returns the converted price
+ */
+ private formatPrice(price: number): number {
+ if (price == null || price == NaN) {
+ return null;
+ } else if (price == 0) {
+ return 0;
+ } else {
+ price = (price / 10.0);
+ return Math.round(price * 10000) / 10000.0;
+ }
+ }
+
+ protected getChannelAddresses(edge: Edge, config: EdgeConfig): Promise {
+ return new Promise((resolve) => {
+ resolve(
+ [
+ new ChannelAddress(this.componentId, 'Delayed'),
+ new ChannelAddress(this.componentId, 'QuarterlyPrices'),
+ new ChannelAddress(this.componentId, 'StateMachine'),
+ new ChannelAddress('_sum', 'EssSoc'),
+ // new ChannelAddress(this.componentId, 'PredictedSocWithoutLogic'),
+ ]);
+ });
+ }
+
+ protected setLabel(config: EdgeConfig) {
+ let options = this.createDefaultChartOptions();
+ let translate = this.translate;
+
+ console.log('options: ', options);
+
+ // Adds second y-axis to chart
+ options.scales.yAxes.push({
+ id: 'yAxis2',
+ position: 'right',
+ scaleLabel: {
+ display: true,
+ labelString: "%",
+ padding: -2,
+ fontSize: 11
+ },
+ gridLines: {
+ display: false
+ },
+ ticks: {
+ beginAtZero: true,
+ max: 100,
+ padding: -5,
+ stepSize: 20
+ }
+ })
+ options.layout = {
+ padding: {
+ left: 2,
+ right: 2,
+ top: 0,
+ bottom: 0
+ }
+ }
+
+ options.scales.xAxes[0].stacked = true;
+
+ //x-axis
+ if (differenceInDays(this.service.historyPeriod.to, this.service.historyPeriod.from) >= 5) {
+ options.scales.xAxes[0].time.unit = "day";
+ } else {
+ options.scales.xAxes[0].time.unit = "hour";
+ }
+
+ //y-axis
+ options.scales.yAxes[0].id = "yAxis1"
+ options.scales.yAxes[0].scaleLabel.labelString = "Cent / kWh";
+ options.scales.yAxes[0].scaleLabel.padding = -2;
+ options.scales.yAxes[0].scaleLabel.fontSize = 11;
+ options.scales.yAxes[0].ticks.padding = -5;
+ options.tooltips.callbacks.label = function (tooltipItem: TooltipItem, data: Data) {
+ let label = data.datasets[tooltipItem.datasetIndex].label;
+ let value = tooltipItem.yLabel;
+
+ if (!value) {
+ return;
+ }
+ if (label == translate.instant('General.soc')) {
+ return label + ": " + formatNumber(value, 'de', '1.0-0') + " %";
+ // } else if (label == 'Predicted Soc without logic') {
+ // return label + ": " + formatNumber(value, 'de', '1.0-0') + " %";
+ } else {
+ return label + ": " + formatNumber(value, 'de', '1.0-4') + " Cent/kWh";
+ }
+ }
+ this.options = options;
+ }
+
+ public getChartHeight(): number {
+ return window.innerHeight / 1.3;
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/edge/history/timeofusetariffdischarge/timeofusetariffdischargeoverview/timeofusetariffdischargechartoverview.component.html b/ui/src/app/edge/history/timeofusetariffdischarge/timeofusetariffdischargeoverview/timeofusetariffdischargechartoverview.component.html
new file mode 100644
index 00000000000..da8d9f66cf4
--- /dev/null
+++ b/ui/src/app/edge/history/timeofusetariffdischarge/timeofusetariffdischargeoverview/timeofusetariffdischargechartoverview.component.html
@@ -0,0 +1,30 @@
+
+
+
+ {{ component.alias }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/app/edge/history/timeofusetariffdischarge/timeofusetariffdischargeoverview/timeofusetariffdischargechartoverview.component.ts b/ui/src/app/edge/history/timeofusetariffdischarge/timeofusetariffdischargeoverview/timeofusetariffdischargechartoverview.component.ts
new file mode 100644
index 00000000000..5cc8201f183
--- /dev/null
+++ b/ui/src/app/edge/history/timeofusetariffdischarge/timeofusetariffdischargeoverview/timeofusetariffdischargechartoverview.component.ts
@@ -0,0 +1,30 @@
+import { ActivatedRoute } from '@angular/router';
+import { Component } from '@angular/core';
+import { Service, Utils, EdgeConfig, Edge } from '../../../../shared/shared';
+
+@Component({
+ selector: TimeOfUseTariffDischargeChartOverviewComponent.SELECTOR,
+ templateUrl: './timeofusetariffdischargechartoverview.component.html'
+})
+export class TimeOfUseTariffDischargeChartOverviewComponent {
+
+ private static readonly SELECTOR = "timeofusetariffdischarge-chart-overview";
+
+ public edge: Edge = null;
+
+ public component: EdgeConfig.Component = null;
+
+ constructor(
+ public service: Service,
+ private route: ActivatedRoute,
+ ) { }
+
+ ngOnInit() {
+ this.service.setCurrentComponent('', this.route).then(edge => {
+ this.service.getConfig().then(config => {
+ this.edge = edge;
+ this.component = config.getComponent(this.route.snapshot.params.componentId);
+ })
+ });
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/edge/history/timeofusetariffdischarge/widget.component.html b/ui/src/app/edge/history/timeofusetariffdischarge/widget.component.html
new file mode 100644
index 00000000000..09384ac3a39
--- /dev/null
+++ b/ui/src/app/edge/history/timeofusetariffdischarge/widget.component.html
@@ -0,0 +1,18 @@
+
+
+
+ {{ component.alias }}
+
+
+
+
+
+ Edge.Index.Widgets.TimeOfUseTariff.delayedDischarge |
+
+ {{ activeTimeOverPeriod | sectohour }}
+ |
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/app/edge/history/timeofusetariffdischarge/widget.component.ts b/ui/src/app/edge/history/timeofusetariffdischarge/widget.component.ts
new file mode 100644
index 00000000000..69ee3c72fcf
--- /dev/null
+++ b/ui/src/app/edge/history/timeofusetariffdischarge/widget.component.ts
@@ -0,0 +1,67 @@
+import { Component, Input, OnChanges, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { DefaultTypes } from 'src/app/shared/service/defaulttypes';
+import { ChannelAddress, Edge, EdgeConfig, Service } from '../../../shared/shared';
+import { AbstractHistoryWidget } from '../abstracthistorywidget';
+
+@Component({
+ selector: TimeOfUseTariffDischargeWidgetComponent.SELECTOR,
+ templateUrl: './widget.component.html'
+})
+export class TimeOfUseTariffDischargeWidgetComponent extends AbstractHistoryWidget implements OnInit, OnChanges {
+
+ @Input() public period: DefaultTypes.HistoryPeriod;
+ @Input() public componentId: string;
+
+ private static readonly SELECTOR = "timeOfUseTariffDischargeWidget";
+
+ public activeTimeOverPeriod: number = null;
+ public edge: Edge = null;
+ public component: EdgeConfig.Component = null;
+
+ constructor(
+ public service: Service,
+ private route: ActivatedRoute,
+ ) {
+ super(service);
+ }
+
+ ngOnInit() {
+ this.service.setCurrentComponent('', this.route).then(response => {
+ this.edge = response;
+ this.service.getConfig().then(config => {
+ this.component = config.getComponent(this.componentId);
+ })
+ });
+ }
+
+ ngOnDestroy() {
+ this.unsubscribeWidgetRefresh()
+ }
+
+ ngOnChanges() {
+ this.updateValues();
+ };
+
+ // Calculate active time based on a time counter
+ protected updateValues() {
+
+ this.service.getConfig().then(config => {
+ this.getChannelAddresses(this.edge, config).then(channels => {
+ this.service.queryEnergy(this.period.from, this.period.to, channels).then(response => {
+ let result = response.result;
+ if (this.componentId + '/DelayedTime' in result.data) {
+ this.activeTimeOverPeriod = result.data[this.componentId + '/DelayedTime'];
+ }
+ })
+ });
+ });
+ }
+
+ protected getChannelAddresses(edge: Edge, config: EdgeConfig): Promise {
+
+ return new Promise((resolve) => {
+ resolve([new ChannelAddress(this.componentId, 'DelayedTime')]);
+ });
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/edge/live/Controller/Channelthreshold/Channelthreshold.ts b/ui/src/app/edge/live/Controller/Channelthreshold/Channelthreshold.ts
index 004909dd9b9..fb137e28382 100644
--- a/ui/src/app/edge/live/Controller/Channelthreshold/Channelthreshold.ts
+++ b/ui/src/app/edge/live/Controller/Channelthreshold/Channelthreshold.ts
@@ -1,6 +1,6 @@
import { ChannelAddress, CurrentData } from '../../../../shared/shared';
import { Component } from '@angular/core';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
import { Icon } from 'src/app/shared/type/widget';
@Component({
diff --git a/ui/src/app/edge/live/Controller/ChpSoc/ChpSoc.ts b/ui/src/app/edge/live/Controller/ChpSoc/ChpSoc.ts
index b2623c0caf9..d30e52b1043 100644
--- a/ui/src/app/edge/live/Controller/ChpSoc/ChpSoc.ts
+++ b/ui/src/app/edge/live/Controller/ChpSoc/ChpSoc.ts
@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { Icon } from 'src/app/shared/type/widget';
import { ChannelAddress, CurrentData } from '../../../../shared/shared';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
import { Controller_ChpSocModalComponent } from './modal/modal.component';
@Component({
diff --git a/ui/src/app/edge/live/Controller/Ess_FixActivePower/Ess_FixActivePower.html b/ui/src/app/edge/live/Controller/Ess_FixActivePower/Ess_FixActivePower.html
deleted file mode 100644
index aeb6ee7422e..00000000000
--- a/ui/src/app/edge/live/Controller/Ess_FixActivePower/Ess_FixActivePower.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/ui/src/app/edge/live/Controller/Ess_FixActivePower/Ess_FixActivePower.ts b/ui/src/app/edge/live/Controller/Ess_FixActivePower/Ess_FixActivePower.ts
index 381f17739b3..e5965505b82 100644
--- a/ui/src/app/edge/live/Controller/Ess_FixActivePower/Ess_FixActivePower.ts
+++ b/ui/src/app/edge/live/Controller/Ess_FixActivePower/Ess_FixActivePower.ts
@@ -1,56 +1,24 @@
-import { Component } from '@angular/core';
-import { ChannelAddress, CurrentData } from 'src/app/shared/shared';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
-import { Controller_Ess_FixActivePowerModalComponent } from './modal/modal.component';
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { SharedModule } from 'src/app/shared/shared.module';
+import { Flat } from './flat/flat';
+import { Modal } from './modal/modal';
-@Component({
- selector: 'Controller_Ess_FixActivePower',
- templateUrl: './Ess_FixActivePower.html'
+@NgModule({
+ imports: [
+ BrowserModule,
+ SharedModule,
+ ],
+ entryComponents: [
+ Flat,
+ Modal,
+ ],
+ declarations: [
+ Flat,
+ Modal,
+ ],
+ exports: [
+ Flat
+ ]
})
-export class Controller_Ess_FixActivePower extends AbstractFlatWidget {
-
- private static PROPERTY_POWER: string = "_PropertyPower";
-
- public chargeState: string;
- public chargeStateValue: number;
-
- public stateConverter = (value: any): string => {
- if (value === 'MANUAL_ON') {
- return this.translate.instant('General.on');
- } else if (value === 'MANUAL_OFF') {
- return this.translate.instant('General.off');
- } else {
- return '-';
- }
- }
-
- protected getChannelAddresses(): ChannelAddress[] {
- let channelAddresses: ChannelAddress[] = [new ChannelAddress(this.componentId, Controller_Ess_FixActivePower.PROPERTY_POWER)]
- return channelAddresses;
- }
-
- protected onCurrentData(currentData: CurrentData) {
- let channelPower = currentData.thisComponent['_PropertyPower'];
- if (channelPower >= 0) {
- this.chargeState = 'General.dischargePower';
- this.chargeStateValue = channelPower
- } else {
- this.chargeState = 'General.chargePower';
- this.chargeStateValue = channelPower * -1;
- }
- }
-
- async presentModal() {
- if (!this.isInitialized) {
- return;
- }
- const modal = await this.modalController.create({
- component: Controller_Ess_FixActivePowerModalComponent,
- componentProps: {
- component: this.component,
- edge: this.edge,
- }
- });
- return await modal.present();
- }
-}
+export class Controller_Ess_FixActivePower { }
diff --git a/ui/src/app/edge/live/Controller/Ess_FixActivePower/flat/flat.html b/ui/src/app/edge/live/Controller/Ess_FixActivePower/flat/flat.html
new file mode 100644
index 00000000000..55b1aa87dc2
--- /dev/null
+++ b/ui/src/app/edge/live/Controller/Ess_FixActivePower/flat/flat.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/app/edge/live/Controller/Ess_FixActivePower/flat/flat.ts b/ui/src/app/edge/live/Controller/Ess_FixActivePower/flat/flat.ts
new file mode 100644
index 00000000000..bdc861657fa
--- /dev/null
+++ b/ui/src/app/edge/live/Controller/Ess_FixActivePower/flat/flat.ts
@@ -0,0 +1,43 @@
+import { Component } from '@angular/core';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
+import { DefaultTypes } from 'src/app/shared/service/defaulttypes';
+import { ChannelAddress, CurrentData, Utils } from 'src/app/shared/shared';
+import { Modal } from '../modal/modal';
+
+@Component({
+ selector: 'Controller_Ess_FixActivePower',
+ templateUrl: './flat.html'
+})
+export class Flat extends AbstractFlatWidget {
+
+ public readonly CONVERT_WATT_TO_KILOWATT = Utils.CONVERT_WATT_TO_KILOWATT;
+ public readonly CONVERT_MANUAL_ON_OFF = Utils.CONVERT_MANUAL_ON_OFF(this.translate);
+
+ public chargeDischargePower: { name: string, value: number };
+ public propertyMode: DefaultTypes.ManualOnOff = null;
+
+ protected override getChannelAddresses(): ChannelAddress[] {
+ return [
+ new ChannelAddress(this.component.id, "_PropertyPower"),
+ new ChannelAddress(this.component.id, "_PropertyMode")
+ ];
+ }
+
+ protected override onCurrentData(currentData: CurrentData) {
+ this.chargeDischargePower = Utils.convertChargeDischargePower(this.translate, currentData.thisComponent['_PropertyPower']);
+ this.propertyMode = currentData.thisComponent['_PropertyMode'];
+ }
+
+ async presentModal() {
+ if (!this.isInitialized) {
+ return;
+ }
+ const modal = await this.modalController.create({
+ component: Modal,
+ componentProps: {
+ component: this.component
+ }
+ });
+ return await modal.present();
+ }
+}
diff --git a/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.component.html b/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.component.html
deleted file mode 100644
index abaa0c685d1..00000000000
--- a/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.component.html
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
- {{ component.alias }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.component.ts b/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.component.ts
deleted file mode 100644
index 5987afa9544..00000000000
--- a/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.component.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { Component, Input } from '@angular/core';
-import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
-import { ModalController } from '@ionic/angular';
-import { TranslateService } from '@ngx-translate/core';
-import { Edge, EdgeConfig, Service, Websocket } from '../../../../../shared/shared';
-
-@Component({
- selector: 'fixactivepower-modal',
- templateUrl: './modal.component.html'
-})
-export class Controller_Ess_FixActivePowerModalComponent {
-
- @Input() public edge: Edge | null = null;
- @Input() public component: EdgeConfig.Component | null = null;
-
- public formGroup: FormGroup;
- public loading: boolean = false;
-
- constructor(
- public modalCtrl: ModalController,
- public service: Service,
- public formBuilder: FormBuilder,
- public websocket: Websocket,
- public translate: TranslateService,
- ) { }
-
- ngOnInit() {
- this.formGroup = this.formBuilder.group({
- mode: new FormControl(this.component.properties.mode),
- power: new FormControl(this.component.properties.power),
- })
- }
-
- public updateControllerMode(event: CustomEvent) {
- let oldMode = this.component.properties['mode'];
- let newMode = event.detail.value;
-
- if (this.edge != null) {
- this.edge.updateComponentConfig(this.websocket, this.component.id, [
- { name: 'mode', value: newMode }
- ]).then(() => {
- this.component.properties.mode = newMode;
- this.formGroup.markAsPristine();
- this.service.toast(this.translate.instant('General.changeAccepted'), 'success');
- }).catch(reason => {
- this.component.properties.mode = oldMode;
- this.service.toast(this.translate.instant('General.changeFailed') + '\n' + reason.error.message, 'danger');
- console.warn(reason);
- });
- }
- }
-
- applyChanges() {
- if (this.edge != null) {
- if (this.edge.roleIsAtLeast('owner')) {
- let updateComponentArray = [];
- Object.keys(this.formGroup.controls).forEach((element, index) => {
- if (this.formGroup.controls[element].dirty) {
- updateComponentArray.push({ name: Object.keys(this.formGroup.controls)[index], value: this.formGroup.controls[element].value })
- }
- })
- this.loading = true;
- this.edge.updateComponentConfig(this.websocket, this.component.id, updateComponentArray).then(() => {
- this.component.properties.mode = this.formGroup.controls['mode'].value;
- this.component.properties.power = this.formGroup.controls['power'].value;
- this.loading = false;
- this.service.toast(this.translate.instant('General.changeAccepted'), 'success');
- }).catch(reason => {
- this.formGroup.controls['mode'].setValue(this.component.properties.mode);
- this.formGroup.controls['power'].setValue(this.component.properties.power);
- this.loading = false;
- this.service.toast(this.translate.instant('General.changeFailed') + '\n' + reason.error.message, 'danger');
- console.warn(reason);
- })
- this.formGroup.markAsPristine()
- } else {
- this.service.toast(this.translate.instant('General.insufficientRights'), 'danger');
- }
- }
- }
-}
\ No newline at end of file
diff --git a/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.html b/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.html
new file mode 100644
index 00000000000..112b76ea758
--- /dev/null
+++ b/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.ts b/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.ts
new file mode 100644
index 00000000000..c49b9d9c576
--- /dev/null
+++ b/ui/src/app/edge/live/Controller/Ess_FixActivePower/modal/modal.ts
@@ -0,0 +1,32 @@
+import { Component } from '@angular/core';
+import { FormControl, FormGroup } from '@angular/forms';
+import { AbstractModal } from 'src/app/shared/genericComponents/modal/abstractModal';
+import { ChannelAddress, CurrentData, Utils } from 'src/app/shared/shared';
+
+@Component({
+ templateUrl: './modal.html'
+})
+export class Modal extends AbstractModal {
+
+ public chargeDischargePower: { name: string, value: number };
+
+ public readonly CONVERT_TO_WATT = Utils.CONVERT_TO_WATT;
+ public readonly CONVERT_MANUAL_ON_OFF = Utils.CONVERT_MANUAL_ON_OFF(this.translate);
+
+ protected override getChannelAddresses(): ChannelAddress[] {
+ return [
+ new ChannelAddress(this.component.id, "_PropertyPower"),
+ ];
+ }
+
+ protected override onCurrentData(currentData: CurrentData) {
+ this.chargeDischargePower = Utils.convertChargeDischargePower(this.translate, currentData.thisComponent['_PropertyPower']);
+ }
+
+ protected override getFormGroup(): FormGroup {
+ return this.formBuilder.group({
+ mode: new FormControl(this.component.properties.mode),
+ power: new FormControl(this.component.properties.power),
+ })
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/Ess_Time-Of-Use-Tariff_Discharge.html b/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/Ess_Time-Of-Use-Tariff_Discharge.html
new file mode 100644
index 00000000000..8920f08d689
--- /dev/null
+++ b/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/Ess_Time-Of-Use-Tariff_Discharge.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/Ess_Time-Of-Use-Tariff_Discharge.ts b/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/Ess_Time-Of-Use-Tariff_Discharge.ts
new file mode 100644
index 00000000000..8a1aec4535b
--- /dev/null
+++ b/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/Ess_Time-Of-Use-Tariff_Discharge.ts
@@ -0,0 +1,72 @@
+import { formatNumber } from '@angular/common';
+import { Component } from '@angular/core';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
+import { ChannelAddress, CurrentData } from 'src/app/shared/shared';
+import { Controller_Ess_TimeOfUseTariff_DischargeModalComponent } from './modal/modal.component';
+
+@Component({
+ selector: 'Controller_Ess_TimeOfUseTariff_Discharge',
+ templateUrl: './Ess_Time-Of-Use-Tariff_Discharge.html'
+})
+export class Controller_Ess_TimeOfUseTariff_Discharge extends AbstractFlatWidget {
+
+ public state: string;
+ public mode: string;
+ public priceConverter = (value: any): string => {
+ if (!value) {
+ return '- Cent/kWh';
+ }
+ return formatNumber(value / 10, 'de', '1.0-2') + ' Cent/kWh'
+ }
+
+ protected onCurrentData(currentData: CurrentData) {
+
+ // State
+ let channelState = currentData.thisComponent['StateMachine'];
+ switch (channelState) {
+ case -1:
+ this.state = this.translate.instant('Edge.Index.Widgets.TimeOfUseTariff.State.notStarted')
+ break;
+ case 0:
+ this.state = this.translate.instant('Edge.Index.Widgets.TimeOfUseTariff.State.delayed')
+ break;
+ case 1:
+ this.state = this.translate.instant('Edge.Index.Widgets.TimeOfUseTariff.State.allowsDischarge')
+ break;
+ case 2:
+ this.state = this.translate.instant('Edge.Index.Widgets.TimeOfUseTariff.State.standby')
+ break;
+ }
+
+ // Mode
+ let modeValue = currentData.allComponents[this.component.id + '/_PropertyMode']
+ switch (modeValue) {
+ case 'OFF':
+ this.mode = this.translate.instant('General.off');
+ break;
+ case 'AUTOMATIC':
+ this.mode = this.translate.instant('General.automatic');
+ }
+ }
+
+ protected getChannelAddresses() {
+ return [
+ new ChannelAddress(this.componentId, 'Delayed'),
+ new ChannelAddress(this.componentId, 'QuarterlyPrices'),
+ new ChannelAddress(this.componentId, 'StateMachine'),
+ new ChannelAddress(this.componentId, '_PropertyMode'),
+ ]
+ }
+
+ async presentModal() {
+ const modal = await this.modalController.create({
+ component: Controller_Ess_TimeOfUseTariff_DischargeModalComponent,
+ componentProps: {
+ component: this.component,
+ edge: this.edge,
+ config: this.config,
+ }
+ });
+ return await modal.present();
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/modal/modal.component.html b/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/modal/modal.component.html
new file mode 100644
index 00000000000..beda8f42142
--- /dev/null
+++ b/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/modal/modal.component.html
@@ -0,0 +1,106 @@
+
+
+ {{ component.alias }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Edge.Index.Widgets.TimeOfUseTariff.storageDischarge
+ |
+
+ {{ getState(currentData[component.id + '/StateMachine'] )}}
+ |
+
+
+
+ Edge.Index.Widgets.TimeOfUseTariff.currentTariff
+ |
+
+ {{currentData[component.id + '/QuarterlyPrices'] / 10 | number: '1.0-4'}} Cent/kWh
+ |
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/modal/modal.component.ts b/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/modal/modal.component.ts
new file mode 100644
index 00000000000..623ba0376c9
--- /dev/null
+++ b/ui/src/app/edge/live/Controller/Ess_Time-Of-Use-Tariff_Discharge/modal/modal.component.ts
@@ -0,0 +1,91 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
+import { ModalController } from '@ionic/angular';
+import { TranslateService } from '@ngx-translate/core';
+import { Edge, EdgeConfig, Service, Websocket } from 'src/app/shared/shared';
+import { Role } from 'src/app/shared/type/role';
+
+type Mode = 'OFF' | 'AUTOMATIC';
+
+@Component({
+ selector: Controller_Ess_TimeOfUseTariff_DischargeModalComponent.SELECTOR,
+ templateUrl: './modal.component.html',
+})
+export class Controller_Ess_TimeOfUseTariff_DischargeModalComponent implements OnInit {
+
+ @Input() public edge: Edge;
+ @Input() public config: EdgeConfig;
+ @Input() public component: EdgeConfig.Component;
+
+ private static readonly SELECTOR = "timeofusetariffdischarge-modal";
+
+ public formGroup: FormGroup;
+ public loading: boolean = false;
+ public pickerOptions: any;
+ public isInstaller: boolean;
+ public refreshChart: boolean;
+
+ constructor(
+ public formBuilder: FormBuilder,
+ public modalCtrl: ModalController,
+ public service: Service,
+ public translate: TranslateService,
+ public websocket: Websocket,
+ ) { }
+
+ ngOnInit() {
+ this.refreshChart = false;
+ if (this.edge.roleIsAtLeast(Role.INSTALLER)) {
+ this.isInstaller = true;
+ }
+ this.formGroup = this.formBuilder.group({
+ mode: new FormControl(this.component.properties.mode),
+ })
+ };
+
+ updateProperty(property: string, event: CustomEvent) {
+ this.formGroup.controls[property].setValue(event.detail.value);
+ this.formGroup.controls[property].markAsDirty()
+ }
+
+ applyChanges() {
+ if (this.edge != null) {
+ if (this.edge.roleIsAtLeast('owner')) {
+ let updateComponentArray = [];
+ Object.keys(this.formGroup.controls).forEach((element, index) => {
+ if (this.formGroup.controls[element].dirty) {
+ updateComponentArray.push({ name: Object.keys(this.formGroup.controls)[index], value: this.formGroup.controls[element].value })
+ }
+ });
+
+ this.loading = true;
+ this.edge.updateComponentConfig(this.websocket, this.component.id, updateComponentArray).then(() => {
+ this.component.properties.mode = this.formGroup.controls['mode'].value;
+ this.loading = false;
+ this.refreshChart = true;
+ this.service.toast(this.translate.instant('General.changeAccepted'), 'success');
+ }).catch(reason => {
+ this.formGroup.controls['mode'].setValue(this.component.properties.mode);
+ this.loading = false;
+ console.warn(reason);
+ });
+ this.formGroup.markAsPristine()
+ } else {
+ this.service.toast(this.translate.instant('General.insufficientRights'), 'danger');
+ }
+ }
+ }
+
+ getState(state: number) {
+ switch (state) {
+ case -1:
+ return this.translate.instant('Edge.Index.Widgets.TimeOfUseTariff.State.notStarted');
+ case 0:
+ return this.translate.instant('Edge.Index.Widgets.TimeOfUseTariff.State.delayed');
+ case 1:
+ return this.translate.instant('Edge.Index.Widgets.TimeOfUseTariff.State.allowsDischarge');
+ case 2:
+ return this.translate.instant('Edge.Index.Widgets.TimeOfUseTariff.State.standby');
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/edge/live/Controller/Evcs/Evcs.ts b/ui/src/app/edge/live/Controller/Evcs/Evcs.ts
index 7b5682224fd..41926e2e8f2 100644
--- a/ui/src/app/edge/live/Controller/Evcs/Evcs.ts
+++ b/ui/src/app/edge/live/Controller/Evcs/Evcs.ts
@@ -1,7 +1,7 @@
import { ChannelAddress, CurrentData, EdgeConfig, Utils } from '../../../../shared/shared';
import { Component } from '@angular/core';
import { Controller_EvcsModalComponent } from './modal/modal.page';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
@Component({
selector: 'Controller_Evcs',
diff --git a/ui/src/app/edge/live/Controller/Io_ChannelSingleThreshold/Io_ChannelSingleThreshold.ts b/ui/src/app/edge/live/Controller/Io_ChannelSingleThreshold/Io_ChannelSingleThreshold.ts
index 6ae31143a30..860a633ef4d 100644
--- a/ui/src/app/edge/live/Controller/Io_ChannelSingleThreshold/Io_ChannelSingleThreshold.ts
+++ b/ui/src/app/edge/live/Controller/Io_ChannelSingleThreshold/Io_ChannelSingleThreshold.ts
@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { Icon } from 'src/app/shared/type/widget';
import { ChannelAddress, CurrentData, Utils } from '../../../../shared/shared';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
import { Controller_Io_ChannelSingleThresholdModalComponent } from './modal/modal.component';
@Component({
diff --git a/ui/src/app/edge/live/Controller/Io_FixDigitalOutput/Io_FixDigitalOutput.ts b/ui/src/app/edge/live/Controller/Io_FixDigitalOutput/Io_FixDigitalOutput.ts
index 91db9c98714..d5cdba6ecb4 100644
--- a/ui/src/app/edge/live/Controller/Io_FixDigitalOutput/Io_FixDigitalOutput.ts
+++ b/ui/src/app/edge/live/Controller/Io_FixDigitalOutput/Io_FixDigitalOutput.ts
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { ChannelAddress, CurrentData } from 'src/app/shared/shared';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
import { Controller_Io_FixDigitalOutputModalComponent } from './modal/modal.component';
diff --git a/ui/src/app/edge/live/Controller/Io_HeatingElement/Io_HeatingElement.ts b/ui/src/app/edge/live/Controller/Io_HeatingElement/Io_HeatingElement.ts
index 0f5fea91628..9d368289a59 100644
--- a/ui/src/app/edge/live/Controller/Io_HeatingElement/Io_HeatingElement.ts
+++ b/ui/src/app/edge/live/Controller/Io_HeatingElement/Io_HeatingElement.ts
@@ -2,7 +2,7 @@ import { ChannelAddress, EdgeConfig, CurrentData, Utils } from '../../../../shar
import { Component } from '@angular/core';
import { Controller_Io_HeatingElementModalComponent } from './modal/modal.component';
import { BehaviorSubject } from 'rxjs';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
@Component({
selector: 'Controller_Io_HeatingElement',
diff --git a/ui/src/app/edge/live/Controller/Io_Heatpump/Io_Heatpump.ts b/ui/src/app/edge/live/Controller/Io_Heatpump/Io_Heatpump.ts
index fd8416b7236..05bc0c9484f 100644
--- a/ui/src/app/edge/live/Controller/Io_Heatpump/Io_Heatpump.ts
+++ b/ui/src/app/edge/live/Controller/Io_Heatpump/Io_Heatpump.ts
@@ -2,7 +2,7 @@ import { BehaviorSubject } from 'rxjs';
import { ChannelAddress, CurrentData, EdgeConfig } from '../../../../shared/shared';
import { Component } from '@angular/core';
import { Controller_Io_HeatpumpModalComponent } from './modal/modal.component';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
@Component({
selector: 'Controller_Io_Heatpump',
diff --git a/ui/src/app/edge/live/Controller/PeakShaving/Asymmetric/Asymmetric.ts b/ui/src/app/edge/live/Controller/PeakShaving/Asymmetric/Asymmetric.ts
index 277894d8d17..2f7b055e09d 100644
--- a/ui/src/app/edge/live/Controller/PeakShaving/Asymmetric/Asymmetric.ts
+++ b/ui/src/app/edge/live/Controller/PeakShaving/Asymmetric/Asymmetric.ts
@@ -2,7 +2,7 @@ import { Controller_Asymmetric_PeakShavingModalComponent } from './modal/modal.c
import { BehaviorSubject } from 'rxjs';
import { ChannelAddress, CurrentData, Utils } from '../../../../../shared/shared';
import { Component } from '@angular/core';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
@Component({
selector: 'Controller_Asymmetric_PeakShaving',
diff --git a/ui/src/app/edge/live/Controller/PeakShaving/Symmetric/Symmetric.ts b/ui/src/app/edge/live/Controller/PeakShaving/Symmetric/Symmetric.ts
index ba12e72780a..71e4772a429 100644
--- a/ui/src/app/edge/live/Controller/PeakShaving/Symmetric/Symmetric.ts
+++ b/ui/src/app/edge/live/Controller/PeakShaving/Symmetric/Symmetric.ts
@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { ChannelAddress, CurrentData, Utils } from '../../../../../shared/shared';
import { Controller_Symmetric_PeakShavingModalComponent } from './modal/modal.component';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
@Component({
selector: 'Controller_Symmetric_PeakShaving',
diff --git a/ui/src/app/edge/live/Controller/PeakShaving/Symmetric_TimeSlot/Symmetric_TimeSlot.ts b/ui/src/app/edge/live/Controller/PeakShaving/Symmetric_TimeSlot/Symmetric_TimeSlot.ts
index 241b35f96fc..21156aeddfe 100644
--- a/ui/src/app/edge/live/Controller/PeakShaving/Symmetric_TimeSlot/Symmetric_TimeSlot.ts
+++ b/ui/src/app/edge/live/Controller/PeakShaving/Symmetric_TimeSlot/Symmetric_TimeSlot.ts
@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { ChannelAddress, CurrentData, Utils } from '../../../../../shared/shared';
import { Controller_Symmetric_TimeSlot_PeakShavingModalComponent } from './modal/modal.component';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
@Component({
selector: 'Controller_Symmetric_TimeSlot_PeakShaving',
diff --git a/ui/src/app/edge/live/Io/Api_DigitalInput/Io_Api_DigitalInput.ts b/ui/src/app/edge/live/Io/Api_DigitalInput/Io_Api_DigitalInput.ts
index 52d797ff9c6..1ef300dce9d 100644
--- a/ui/src/app/edge/live/Io/Api_DigitalInput/Io_Api_DigitalInput.ts
+++ b/ui/src/app/edge/live/Io/Api_DigitalInput/Io_Api_DigitalInput.ts
@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { Io_Api_DigitalInput_ModalComponent } from './modal/modal.component';
import { ChannelAddress, EdgeConfig } from 'src/app/shared/shared';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
@Component({
selector: 'Io_Api_DigitalInput',
diff --git a/ui/src/app/edge/live/Multiple/Evcs_Api_Cluster/Evcs_Api_Cluster.ts b/ui/src/app/edge/live/Multiple/Evcs_Api_Cluster/Evcs_Api_Cluster.ts
index b6a6883edeb..5b6c2a79fcf 100644
--- a/ui/src/app/edge/live/Multiple/Evcs_Api_Cluster/Evcs_Api_Cluster.ts
+++ b/ui/src/app/edge/live/Multiple/Evcs_Api_Cluster/Evcs_Api_Cluster.ts
@@ -1,7 +1,7 @@
import { ChannelAddress, CurrentData, EdgeConfig, Utils } from '../../../../shared/shared';
import { Component } from '@angular/core';
import { Evcs_Api_ClusterModalComponent } from './modal/evcsCluster-modal.page';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
@Component({
selector: 'Evcs_Api_Cluster',
diff --git a/ui/src/app/edge/live/common/autarchy/Common_Autarchy.ts b/ui/src/app/edge/live/common/autarchy/Common_Autarchy.ts
new file mode 100644
index 00000000000..44629bce56f
--- /dev/null
+++ b/ui/src/app/edge/live/common/autarchy/Common_Autarchy.ts
@@ -0,0 +1,24 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { SharedModule } from 'src/app/shared/shared.module';
+import { Flat } from './flat/flat';
+import { Modal } from './modal/modal';
+
+@NgModule({
+ imports: [
+ BrowserModule,
+ SharedModule,
+ ],
+ entryComponents: [
+ Flat,
+ Modal,
+ ],
+ declarations: [
+ Flat,
+ Modal,
+ ],
+ exports: [
+ Flat
+ ]
+})
+export class Common_Autarchy { }
diff --git a/ui/src/app/edge/live/common/autarchy/autarchy.component.ts b/ui/src/app/edge/live/common/autarchy/autarchy.component.ts
deleted file mode 100644
index df9809a3027..00000000000
--- a/ui/src/app/edge/live/common/autarchy/autarchy.component.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { Component } from '@angular/core';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
-import { ChannelAddress, CurrentData } from '../../../../shared/shared';
-import { AutarchyModalComponent } from './modal/modal.component';
-
-@Component({
- selector: 'autarchy',
- templateUrl: './autarchy.component.html'
-})
-export class AutarchyComponent extends AbstractFlatWidget {
-
- public percentageValue: number;
-
- private static readonly SUM_GRID_ACTIVE_POWER: ChannelAddress = new ChannelAddress('_sum', 'GridActivePower');
- private static readonly SUM_CONSUMPTION_ACTIVE_POWER: ChannelAddress = new ChannelAddress('_sum', 'ConsumptionActivePower');
-
- protected getChannelAddresses(): ChannelAddress[] {
- return [
- AutarchyComponent.SUM_GRID_ACTIVE_POWER,
- AutarchyComponent.SUM_CONSUMPTION_ACTIVE_POWER,
- ];
- }
-
- protected onCurrentData(currentData: CurrentData) {
- this.percentageValue = this.calculateAutarchy(
- currentData.allComponents[AutarchyComponent.SUM_GRID_ACTIVE_POWER.toString()],
- currentData.allComponents[AutarchyComponent.SUM_CONSUMPTION_ACTIVE_POWER.toString()]
- );
- }
-
- private calculateAutarchy(buyFromGrid: number, consumptionActivePower: number): number | null {
- if (buyFromGrid != null && consumptionActivePower != null) {
- if (consumptionActivePower <= 0) {
- /* avoid divide by zero; consumption == 0 -> autarchy 100 % */
- return 100;
-
- } else {
- return /* min 0 */ Math.max(0,
- /* max 100 */ Math.min(100,
- /* calculate autarchy */(1 - buyFromGrid / consumptionActivePower) * 100
- ));
- }
-
- } else {
- return null;
- }
- }
-
- async presentModal() {
- const modal = await this.modalController.create({
- component: AutarchyModalComponent,
- });
- return await modal.present();
- }
-
-}
diff --git a/ui/src/app/edge/live/common/autarchy/autarchy.component.html b/ui/src/app/edge/live/common/autarchy/flat/flat.html
similarity index 100%
rename from ui/src/app/edge/live/common/autarchy/autarchy.component.html
rename to ui/src/app/edge/live/common/autarchy/flat/flat.html
diff --git a/ui/src/app/edge/live/common/autarchy/flat/flat.ts b/ui/src/app/edge/live/common/autarchy/flat/flat.ts
new file mode 100644
index 00000000000..29509b5449a
--- /dev/null
+++ b/ui/src/app/edge/live/common/autarchy/flat/flat.ts
@@ -0,0 +1,53 @@
+import { Component } from '@angular/core';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
+import { ChannelAddress, CurrentData } from 'src/app/shared/shared';
+import { Modal } from '../modal/modal';
+
+@Component({
+ selector: 'Common_Autarchy',
+ templateUrl: './flat.html'
+})
+export class Flat extends AbstractFlatWidget {
+
+ public percentageValue: number;
+
+ protected override getChannelAddresses(): ChannelAddress[] {
+ return [
+ new ChannelAddress('_sum', 'GridActivePower'),
+ new ChannelAddress('_sum', 'ConsumptionActivePower'),
+ ];
+ }
+
+ protected override onCurrentData(currentData: CurrentData) {
+ this.percentageValue = this.calculateAutarchy(
+ currentData.allComponents['_sum/GridActivePower'],
+ currentData.allComponents['_sum/ConsumptionActivePower']
+ );
+ }
+
+ private calculateAutarchy(buyFromGrid: number, consumptionActivePower: number): number | null {
+ if (buyFromGrid != null && consumptionActivePower != null) {
+ if (consumptionActivePower <= 0) {
+ /* avoid divide by zero; consumption == 0 -> autarchy 100 % */
+ return 100;
+
+ } else {
+ return /* min 0 */ Math.max(0,
+ /* max 100 */ Math.min(100,
+ /* calculate autarchy */(1 - buyFromGrid / consumptionActivePower) * 100
+ ));
+ }
+
+ } else {
+ return null;
+ }
+ }
+
+ async presentModal() {
+ const modal = await this.modalController.create({
+ component: Modal,
+ });
+ return await modal.present();
+ }
+
+}
diff --git a/ui/src/app/edge/live/common/autarchy/modal/modal.component.html b/ui/src/app/edge/live/common/autarchy/modal/modal.component.html
deleted file mode 100644
index 577ac5ca75f..00000000000
--- a/ui/src/app/edge/live/common/autarchy/modal/modal.component.html
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
- General.autarchy
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Edge.Index.Widgets.autarchyInfo
-
-
-
-
-
\ No newline at end of file
diff --git a/ui/src/app/edge/live/common/autarchy/modal/modal.component.ts b/ui/src/app/edge/live/common/autarchy/modal/modal.component.ts
deleted file mode 100644
index 34419239aad..00000000000
--- a/ui/src/app/edge/live/common/autarchy/modal/modal.component.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Component } from '@angular/core';
-import { ModalController } from '@ionic/angular';
-import { Service } from '../../../../../shared/shared';
-
-@Component({
- selector: AutarchyModalComponent.SELECTOR,
- templateUrl: './modal.component.html'
-})
-export class AutarchyModalComponent {
-
- private static readonly SELECTOR = "autarchy-modal";
-
- constructor(
- public modalCtrl: ModalController,
- public service: Service,
- ) { }
-
-}
\ No newline at end of file
diff --git a/ui/src/app/edge/live/common/autarchy/modal/modal.html b/ui/src/app/edge/live/common/autarchy/modal/modal.html
new file mode 100644
index 00000000000..d8544d75e09
--- /dev/null
+++ b/ui/src/app/edge/live/common/autarchy/modal/modal.html
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/app/edge/live/common/autarchy/modal/modal.ts b/ui/src/app/edge/live/common/autarchy/modal/modal.ts
new file mode 100644
index 00000000000..cdb1b8f157b
--- /dev/null
+++ b/ui/src/app/edge/live/common/autarchy/modal/modal.ts
@@ -0,0 +1,6 @@
+import { Component } from '@angular/core';
+
+@Component({
+ templateUrl: './modal.html'
+})
+export class Modal { }
\ No newline at end of file
diff --git a/ui/src/app/edge/live/common/consumption/consumption.component.ts b/ui/src/app/edge/live/common/consumption/consumption.component.ts
index c2b97bd7e3a..bfa5609028c 100644
--- a/ui/src/app/edge/live/common/consumption/consumption.component.ts
+++ b/ui/src/app/edge/live/common/consumption/consumption.component.ts
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { ChannelAddress, CurrentData, EdgeConfig, Utils } from '../../../../shared/shared';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
import { ConsumptionModalComponent } from './modal/modal.component';
@Component({
diff --git a/ui/src/app/edge/live/common/grid/grid.component.ts b/ui/src/app/edge/live/common/grid/grid.component.ts
index 48d48b3bba2..4b6f5b67598 100644
--- a/ui/src/app/edge/live/common/grid/grid.component.ts
+++ b/ui/src/app/edge/live/common/grid/grid.component.ts
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { ChannelAddress, CurrentData, GridMode, Utils } from 'src/app/shared/shared';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
import { GridModalComponent } from './modal/modal.component';
@Component({
diff --git a/ui/src/app/edge/live/common/selfconsumption/Common_Selfconsumption.ts b/ui/src/app/edge/live/common/selfconsumption/Common_Selfconsumption.ts
new file mode 100644
index 00000000000..43754f53491
--- /dev/null
+++ b/ui/src/app/edge/live/common/selfconsumption/Common_Selfconsumption.ts
@@ -0,0 +1,24 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { SharedModule } from 'src/app/shared/shared.module';
+import { Flat } from './flat/flat';
+import { Modal } from './modal/modal';
+
+@NgModule({
+ imports: [
+ BrowserModule,
+ SharedModule,
+ ],
+ entryComponents: [
+ Flat,
+ Modal,
+ ],
+ declarations: [
+ Flat,
+ Modal,
+ ],
+ exports: [
+ Flat
+ ]
+})
+export class Common_Selfconsumption { }
diff --git a/ui/src/app/edge/live/common/selfconsumption/selfconsumption.component.html b/ui/src/app/edge/live/common/selfconsumption/flat/flat.html
similarity index 100%
rename from ui/src/app/edge/live/common/selfconsumption/selfconsumption.component.html
rename to ui/src/app/edge/live/common/selfconsumption/flat/flat.html
diff --git a/ui/src/app/edge/live/common/selfconsumption/flat/flat.ts b/ui/src/app/edge/live/common/selfconsumption/flat/flat.ts
new file mode 100644
index 00000000000..558179b2649
--- /dev/null
+++ b/ui/src/app/edge/live/common/selfconsumption/flat/flat.ts
@@ -0,0 +1,37 @@
+import { Component } from '@angular/core';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
+import { ChannelAddress, CurrentData, Utils } from 'src/app/shared/shared';
+import { Modal } from '../modal/modal';
+
+@Component({
+ selector: 'Common_Selfconsumption',
+ templateUrl: './flat.html'
+})
+export class Flat extends AbstractFlatWidget {
+
+ public calculatedSelfConsumption: number;
+
+ protected getChannelAddresses() {
+ return [
+ new ChannelAddress('_sum', 'GridActivePower'),
+ new ChannelAddress('_sum', 'ProductionActivePower')
+ ];
+ }
+
+ protected onCurrentData(currentData: CurrentData) {
+ this.calculatedSelfConsumption = Utils.calculateSelfConsumption(
+ Utils.multiplySafely(
+ currentData.allComponents['_sum/GridActivePower'],
+ -1
+ ),
+ currentData.allComponents['_sum/ProductionActivePower']
+ )
+ }
+
+ async presentModal() {
+ const modal = await this.modalController.create({
+ component: Modal,
+ });
+ return await modal.present();
+ }
+}
diff --git a/ui/src/app/edge/live/common/selfconsumption/modal/modal.component.html b/ui/src/app/edge/live/common/selfconsumption/modal/modal.component.html
deleted file mode 100644
index 9a257651f84..00000000000
--- a/ui/src/app/edge/live/common/selfconsumption/modal/modal.component.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
- General.selfConsumption
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Edge.Index.Widgets.selfconsumptionInfo
-
-
-
-
-
\ No newline at end of file
diff --git a/ui/src/app/edge/live/common/selfconsumption/modal/modal.component.ts b/ui/src/app/edge/live/common/selfconsumption/modal/modal.component.ts
deleted file mode 100644
index bdaeb6e3758..00000000000
--- a/ui/src/app/edge/live/common/selfconsumption/modal/modal.component.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Component } from '@angular/core';
-import { ModalController } from '@ionic/angular';
-import { Service } from '../../../../../shared/shared';
-
-@Component({
- selector: 'selfconsumption-modal',
- templateUrl: './modal.component.html'
-})
-export class SelfconsumptionModalComponent {
-
- constructor(
- public modalCtrl: ModalController,
- public service: Service,
- ) { }
-}
\ No newline at end of file
diff --git a/ui/src/app/edge/live/common/selfconsumption/modal/modal.html b/ui/src/app/edge/live/common/selfconsumption/modal/modal.html
new file mode 100644
index 00000000000..2d707717e76
--- /dev/null
+++ b/ui/src/app/edge/live/common/selfconsumption/modal/modal.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/ui/src/app/edge/live/common/selfconsumption/modal/modal.ts b/ui/src/app/edge/live/common/selfconsumption/modal/modal.ts
new file mode 100644
index 00000000000..20566aed8fc
--- /dev/null
+++ b/ui/src/app/edge/live/common/selfconsumption/modal/modal.ts
@@ -0,0 +1,6 @@
+import { Component } from '@angular/core';
+
+@Component({
+ templateUrl: './modal.html'
+})
+export class Modal { }
\ No newline at end of file
diff --git a/ui/src/app/edge/live/common/selfconsumption/selfconsumption.component.ts b/ui/src/app/edge/live/common/selfconsumption/selfconsumption.component.ts
deleted file mode 100644
index f87ff259464..00000000000
--- a/ui/src/app/edge/live/common/selfconsumption/selfconsumption.component.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Component } from '@angular/core';
-import { ChannelAddress, CurrentData, Utils } from 'src/app/shared/shared';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
-import { SelfconsumptionModalComponent } from './modal/modal.component';
-
-@Component({
- selector: 'selfconsumption',
- templateUrl: './selfconsumption.component.html'
-})
-export class SelfConsumptionComponent extends AbstractFlatWidget {
-
- private static readonly SUM_GRID_ACTIVE_POWER: ChannelAddress = new ChannelAddress('_sum', 'GridActivePower')
- private static readonly SUM_PRODUCTION_ACTIVE_POWER: ChannelAddress = new ChannelAddress('_sum', 'ProductionActivePower')
- public calculatedSelfConsumption: number;
-
- protected getChannelAddresses() {
- return [SelfConsumptionComponent.SUM_GRID_ACTIVE_POWER, SelfConsumptionComponent.SUM_PRODUCTION_ACTIVE_POWER]
- }
-
- protected onCurrentData(currentData: CurrentData) {
- this.calculatedSelfConsumption = Utils.calculateSelfConsumption(
- Utils.multiplySafely(currentData.allComponents[SelfConsumptionComponent.SUM_GRID_ACTIVE_POWER.toString()], -1),
- currentData.allComponents[SelfConsumptionComponent.SUM_PRODUCTION_ACTIVE_POWER.toString()])
- }
-
- async presentModal() {
- const modal = await this.modalController.create({
- component: SelfconsumptionModalComponent,
- });
- return await modal.present();
- }
-}
diff --git a/ui/src/app/edge/live/common/storage/modal/modal.component.html b/ui/src/app/edge/live/common/storage/modal/modal.component.html
index 078558aedeb..ae9d9248be9 100644
--- a/ui/src/app/edge/live/common/storage/modal/modal.component.html
+++ b/ui/src/app/edge/live/common/storage/modal/modal.component.html
@@ -179,10 +179,11 @@
Edge.Index.EmergencyReserve.emergencyReserve
|
-
+ |
{{ formGroup.value[component.id]?.reserveSoc | unitvalue:'%' }}
|
-
+ |
diff --git a/ui/src/app/edge/live/common/storage/storage.component.ts b/ui/src/app/edge/live/common/storage/storage.component.ts
index 2dbae382ff6..6572590c45d 100644
--- a/ui/src/app/edge/live/common/storage/storage.component.ts
+++ b/ui/src/app/edge/live/common/storage/storage.component.ts
@@ -2,7 +2,7 @@ import { formatNumber } from '@angular/common';
import { Component } from '@angular/core';
import { CurrentData } from "src/app/shared/shared";
import { ChannelAddress, EdgeConfig, Utils } from '../../../../shared/shared';
-import { AbstractFlatWidget } from 'src/app/shared/Generic_Components/flat/abstract-flat-widget';
+import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget';
import { StorageModalComponent } from './modal/modal.component';
@Component({
diff --git a/ui/src/app/edge/live/live.component.html b/ui/src/app/edge/live/live.component.html
index e315f952fa2..8fe7a36b693 100644
--- a/ui/src/app/edge/live/live.component.html
+++ b/ui/src/app/edge/live/live.component.html
@@ -16,12 +16,12 @@
-
-
+
+
-
-
+
+
@@ -117,9 +117,10 @@
-
-
+
+
- OpenEMS-Edge Version
- {{ edge.version }}
+ {{ environment.edgeShortName }} Version
+ {{ edge.version }}
Rolle
diff --git a/ui/src/app/edge/settings/settings.component.html b/ui/src/app/edge/settings/settings.component.html
index 982ab62c8da..3ff0c3b7d8b 100644
--- a/ui/src/app/edge/settings/settings.component.html
+++ b/ui/src/app/edge/settings/settings.component.html
@@ -102,6 +102,48 @@
+
+
+
+
+ {{ environment.edgeShortName }}-App Assistent
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ environment.edgeShortName }} Systemupdate
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ environment.edgeShortName }} Soltaro Service Assistent
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/app/edge/settings/settings.module.ts b/ui/src/app/edge/settings/settings.module.ts
index aa0f8604daf..1912db95fa7 100644
--- a/ui/src/app/edge/settings/settings.module.ts
+++ b/ui/src/app/edge/settings/settings.module.ts
@@ -10,10 +10,11 @@ import { AliasUpdateComponent } from './profile/aliasupdate.component';
import { ProfileComponent } from './profile/profile.component';
import { SettingsComponent } from './settings.component';
import { SystemExecuteComponent } from './systemexecute/systemexecute.component';
+import { SystemUpdateComponent } from './systemupdate/systemupdate.component';
@NgModule({
imports: [
- SharedModule
+ SharedModule,
],
declarations: [
AliasUpdateComponent,
@@ -26,6 +27,7 @@ import { SystemExecuteComponent } from './systemexecute/systemexecute.component'
ProfileComponent,
SettingsComponent,
SystemExecuteComponent,
+ SystemUpdateComponent,
],
entryComponents: []
})
diff --git a/ui/src/app/edge/settings/systemupdate/executeSystemUpdateRequest.ts b/ui/src/app/edge/settings/systemupdate/executeSystemUpdateRequest.ts
new file mode 100644
index 00000000000..cc5d3443fc1
--- /dev/null
+++ b/ui/src/app/edge/settings/systemupdate/executeSystemUpdateRequest.ts
@@ -0,0 +1,29 @@
+import { JsonrpcRequest } from "src/app/shared/jsonrpc/base";
+
+/**
+ * Represents a JSON-RPC Request to execute a system update on OpenEMS Edge.
+ *
+ *
+ * {
+ * "jsonrpc": "2.0",
+ * "id": "UUID",
+ * "method": "executeSystemUpdate",
+ * "params": {
+ * "isDebug": boolean
+ * }
+ * }
+ *
+ */
+export class ExecuteSystemUpdateRequest extends JsonrpcRequest {
+
+ static METHOD: string = "executeSystemUpdate";
+
+ public constructor(
+ public readonly params: {
+ isDebug: boolean
+ }
+ ) {
+ super(ExecuteSystemUpdateRequest.METHOD, params);
+ }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/edge/settings/systemupdate/getSystemUpdateStateRequest.ts b/ui/src/app/edge/settings/systemupdate/getSystemUpdateStateRequest.ts
new file mode 100644
index 00000000000..94092685162
--- /dev/null
+++ b/ui/src/app/edge/settings/systemupdate/getSystemUpdateStateRequest.ts
@@ -0,0 +1,25 @@
+import { JsonrpcRequest } from "src/app/shared/jsonrpc/base";
+
+/**
+ * Represents a JSON-RPC Request to get the current state of system update on OpenEMS Edge.
+ *
+ *
+ * {
+ * "jsonrpc": "2.0",
+ * "id": "UUID",
+ * "method": "getSystemUpdateState",
+ * "params": {
+ * }
+ * }
+ *
+ */
+export class GetSystemUpdateStateRequest extends JsonrpcRequest {
+
+ static METHOD: string = "getSystemUpdateState";
+
+ public constructor(
+ ) {
+ super(GetSystemUpdateStateRequest.METHOD, {});
+ }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/edge/settings/systemupdate/getSystemUpdateStateResponse.ts b/ui/src/app/edge/settings/systemupdate/getSystemUpdateStateResponse.ts
new file mode 100644
index 00000000000..d6ed70243e9
--- /dev/null
+++ b/ui/src/app/edge/settings/systemupdate/getSystemUpdateStateResponse.ts
@@ -0,0 +1,56 @@
+import { JsonrpcResponseSuccess } from "src/app/shared/jsonrpc/base";
+
+export interface SystemUpdateState {
+ unknown?: {},
+ updated?: { version: string },
+ available?: {
+ currentVersion: string,
+ latestVersion: string
+ },
+ running?: {
+ percentCompleted: number,
+ logs: string[]
+ }
+}
+
+/**
+ * JSON-RPC Response to "getSystemUpdateState" Request.
+ *
+ *
+ *
+ *
+ * {
+ * "jsonrpc": "2.0",
+ * "id": "UUID",
+ * "result": {
+ * // State is unknown (e.g. internet connection limited by firewall)
+ * // TODO remove unknown? Throw exception instead
+ * "unknown"?: {
+ * }
+ * // Latest version is already installed
+ * "updated"?: {
+ * "version": "XXXX"
+ * }
+ * // Update is available
+ * "available"?: {
+ * "currentVersion": "XXXX",
+ * "latestVersion": "XXXX"
+ * },
+ * // Update is currently running
+ * "running"?: {
+ * "percentCompleted": number,
+ * "logs": string[]
+ * }
+ * }
+ * }
+ *
+ */
+export class GetSystemUpdateStateResponse extends JsonrpcResponseSuccess {
+
+ public constructor(
+ public readonly id: string,
+ public readonly result: SystemUpdateState
+ ) {
+ super(id, result);
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/edge/settings/systemupdate/systemupdate.component.html b/ui/src/app/edge/settings/systemupdate/systemupdate.component.html
new file mode 100644
index 00000000000..55c8bba11b1
--- /dev/null
+++ b/ui/src/app/edge/settings/systemupdate/systemupdate.component.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+ {{ edge.id }} ist nicht online!
+
+
+
+
+
+
+
+
+
+
+
+
+ System Update
+ für {{ edge.id }}
+
+
+
+
+
+
+
+
+
+ Update Status ist unbekannt
+
+
+ {{ state | json }}
+
+
+
+
+
+ Installierte Version:
+ {{ state.version }}
+
+
+ Das System ist auf dem aktuellsten Softwarestand
+
+
+
+
+
+ Installierte Version:
+ {{ state.currentVersion }}
+
+
+ Neueste Version:
+ {{ state.latestVersion }}
+
+
+
+
+ Neueste Version installieren
+
+
+
+
+
+
+
+
+ Update wird ausgeführt...
+
+ Update abgeschlossen
+
+
+
+
+
+
+
+
+
+ 0">
+
+
+
+
+
+ Details
+
+
+
+ {{ log }}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/app/edge/settings/systemupdate/systemupdate.component.ts b/ui/src/app/edge/settings/systemupdate/systemupdate.component.ts
new file mode 100644
index 00000000000..d107bbd8d75
--- /dev/null
+++ b/ui/src/app/edge/settings/systemupdate/systemupdate.component.ts
@@ -0,0 +1,100 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { Subject, timer } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { ComponentJsonApiRequest } from 'src/app/shared/jsonrpc/request/componentJsonApiRequest';
+import { environment } from 'src/environments';
+import { Edge, Service, Utils, Websocket } from '../../../shared/shared';
+import { ExecuteSystemUpdateRequest } from './executeSystemUpdateRequest';
+import { GetSystemUpdateStateRequest } from './getSystemUpdateStateRequest';
+import { GetSystemUpdateStateResponse, SystemUpdateState } from './getSystemUpdateStateResponse';
+
+@Component({
+ selector: SystemUpdateComponent.SELECTOR,
+ templateUrl: './systemupdate.component.html'
+})
+export class SystemUpdateComponent implements OnInit, OnDestroy {
+
+ private static readonly SELECTOR = "systemUpdate";
+
+ public readonly environment = environment;
+ public systemUpdateState: SystemUpdateState = null;
+ public readonly spinnerId: string = SystemUpdateComponent.SELECTOR;
+ public showLog: boolean = false;
+
+ public edge: Edge = null;
+ private ngUnsubscribe = new Subject();
+
+ constructor(
+ private route: ActivatedRoute,
+ protected utils: Utils,
+ private websocket: Websocket,
+ private service: Service,
+ ) { }
+
+ ngOnInit() {
+ this.service.setCurrentComponent("", this.route).then(edge => {
+ this.edge = edge;
+ // Update System Update State now and every 15 seconds
+ const source = timer(0, 15000);
+ source.pipe(
+ takeUntil(this.ngUnsubscribe)
+ ).subscribe(ignore => {
+ if (!edge.isOnline) {
+ return;
+ }
+ this.refreshSystemUpdateState();
+ });
+ });
+ }
+
+ ngOnDestroy() {
+ this.stopRefreshSystemUpdateState();
+ }
+
+ private refreshSystemUpdateState() {
+ this.service.startSpinner(this.spinnerId);
+ this.edge.sendRequest(this.websocket,
+ new ComponentJsonApiRequest({
+ componentId: "_host",
+ payload: new GetSystemUpdateStateRequest()
+ })).then(response => {
+ let result = (response as GetSystemUpdateStateResponse).result;
+ this.systemUpdateState = result;
+ this.service.stopSpinner(this.spinnerId);
+
+ // Stop regular check if there is no Update available
+ if (result.updated || result.running?.percentCompleted == 100) {
+ this.stopRefreshSystemUpdateState();
+ }
+
+ }).catch(reason => {
+ console.error(reason.error);
+ this.service.toast("Error while executing system update: " + reason.error.message, 'danger');
+ });
+ }
+
+ public executeSystemUpdate() {
+ this.service.startSpinner(this.spinnerId);
+
+ this.edge.sendRequest(this.websocket,
+ new ComponentJsonApiRequest({
+ componentId: "_host",
+ payload: new ExecuteSystemUpdateRequest({ isDebug: environment.debugMode })
+ })).then(response => {
+ // Finished System Update (without restart of OpenEMS Edge)
+ this.systemUpdateState = (response as GetSystemUpdateStateResponse).result;
+ this.service.stopSpinner(this.spinnerId);
+ this.stopRefreshSystemUpdateState();
+
+ }).catch(reason => {
+ console.error(reason.error);
+ this.service.toast("Error while executing system update: " + reason.error.message, 'danger');
+ });
+ }
+
+ private stopRefreshSystemUpdateState() {
+ this.ngUnsubscribe.next();
+ this.ngUnsubscribe.complete();
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/index/index.component.html b/ui/src/app/index/index.component.html
index 84f28af484b..fe40395a2d2 100644
--- a/ui/src/app/index/index.component.html
+++ b/ui/src/app/index/index.component.html
@@ -22,14 +22,14 @@
|