diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000000..a1be5e311ce
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,59 @@
+name: Build OpenEMS
+on: [push]
+jobs:
+ build-code:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup Java 8
+ uses: actions/setup-java@v1
+ with:
+ java-version: '8'
+
+ - name: Setup Cache for Java/Gradle
+ uses: actions/cache@v2
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: ${{ runner.os }}-gradle-
+
+ - name: Setup Cache for Java/Maven
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v1
+ with:
+ node-version: '14'
+
+ - name: Setup Cache for Node.js
+ uses: actions/cache@v2
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: ${{ runner.os }}-node-
+
+ - name: Build all Java packages
+ run: ./gradlew build
+
+ - name: Resolve OpenEMS Backend bundles
+ run: ./gradlew resolve.BackendApp
+
+ - name: Validate BackendApp.bndrun
+ run: git diff --exit-code io.openems.backend.application/BackendApp.bndrun
+
+ - name: Resolve OpenEMS Edge bundles
+ run: ./gradlew resolve.EdgeApp
+
+ - name: Validate EdgeApp.bndrun
+ run: git diff --exit-code io.openems.edge.application/EdgeApp.bndrun
+
+ - name: Build OpenEMS UI
+ run: ./gradlew buildUiForEdge --continue
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 00000000000..51f1ff405d1
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,25 @@
+name: Build Docs
+on:
+ push:
+ branches:
+ - develop
+
+jobs:
+ build-docs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Build Javadocs
+ run: ./gradlew buildAggregatedJavadocs --continue
+
+ - name: Build Antora-docs for openems.io
+ run: ./gradlew buildAntoraDocs --continue
+
+ - name: Deploy to GitHub pages
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ personal_token: ${{ secrets.DOCS }}
+ external_repository: OpenEMS/openems.io
+ publish_branch: master
+ publish_dir: build/www
diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile
index 3830ce11316..e83d1c81679 100644
--- a/.gitpod.Dockerfile
+++ b/.gitpod.Dockerfile
@@ -6,4 +6,7 @@ RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
# disable angular analytics
ENV NG_CLI_ANALYTICS=false
+# Docker build does not rebuild an image when a base image is changed, increase this counter to trigger it.
+ENV TRIGGER_REBUILD 3
+
RUN npm install -g @angular/cli
diff --git a/.gradle-wrapper/gradle-wrapper.properties b/.gradle-wrapper/gradle-wrapper.properties
index da9702f9e70..442d9132ea3 100644
--- a/.gradle-wrapper/gradle-wrapper.properties
+++ b/.gradle-wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index d663fe00f1e..00000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-sudo: false
-
-matrix:
- include:
- - language: java
- jdk:
- - openjdk8
- script:
- - ./gradlew build
- - ./gradlew resolve.BackendApp
- - git diff --exit-code io.openems.backend.application/BackendApp.bndrun
- - ./gradlew resolve.EdgeApp
- - git diff --exit-code io.openems.edge.application/EdgeApp.bndrun
- - ./gradlew buildAggregatedJavadocs --continue
- - ./gradlew buildAntoraDocs --continue
- deploy:
- provider: pages
- skip-cleanup: true
- github-token: $GITHUB_TOKEN
- keep-history: true
- on:
- branch: develop
- repo: OpenEMS/openems.io
- target-branch: master
- local-dir: build/www
-
- - language: node_js
- node_js: lts/*
- script:
- - ./gradlew buildUiForEdge --continue
-
diff --git a/README.md b/README.md
index f037d915a8a..cd4c200c366 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![Build Status](https://www.travis-ci.com/OpenEMS/openems.svg?branch=develop)](https://www.travis-ci.com/OpenEMS/openems)
+[![Build Status](https://github.com/OpenEMS/openems/actions/workflows/build.yml/badge.svg)](https://github.com/OpenEMS/openems/actions/workflows/build.yml)
[![Gitpod live-demo](https://img.shields.io/badge/Gitpod-live--demo-blue?logo=gitpod)](https://gitpod.io/#https://github.com/OpenEMS/openems/tree/master)
[![Cite via Zenodo](https://zenodo.org/badge/DOI/10.5281/zenodo.4440884.svg)](https://doi.org/10.5281/zenodo.4440883)
diff --git a/build.gradle b/build.gradle
index ea1a3fdaaa8..3a3dbc25abd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -69,6 +69,7 @@ task buildAggregatedJavadocs(type: Javadoc, description: 'Generate javadocs from
title = "OpenEMS Javadoc"
subprojects.each { proj ->
proj.tasks.withType(Javadoc).each { javadocTask ->
+ options.addStringOption('Xdoclint:none', '-quiet')
source += javadocTask.source
classpath += javadocTask.classpath
excludes += javadocTask.excludes
diff --git a/cnf/checkstyle.xml b/cnf/checkstyle.xml
index 66dbd3c0f71..bd1b163addc 100644
--- a/cnf/checkstyle.xml
+++ b/cnf/checkstyle.xml
@@ -120,7 +120,7 @@
diff --git a/io.openems.common/src/io/openems/common/utils/UuidUtils.java b/io.openems.common/src/io/openems/common/utils/UuidUtils.java
index ac9e02364fe..19d2e6e5611 100644
--- a/io.openems.common/src/io/openems/common/utils/UuidUtils.java
+++ b/io.openems.common/src/io/openems/common/utils/UuidUtils.java
@@ -7,7 +7,10 @@ public class UuidUtils {
/**
* Create a 'Nil' UUID: "00000000-0000-0000-0000-000000000000".
*
- * @see https://en.wikipedia.org/wiki/Universally_unique_identifier#Nil_UUID
+ *
+ *
+ * @see Wikipedia
*
* @return a Nil UUID
*/
diff --git a/io.openems.common/test/io/openems/common/types/ChannelAddressTest.java b/io.openems.common/test/io/openems/common/types/ChannelAddressTest.java
new file mode 100644
index 00000000000..17bc159def9
--- /dev/null
+++ b/io.openems.common/test/io/openems/common/types/ChannelAddressTest.java
@@ -0,0 +1,31 @@
+package io.openems.common.types;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class ChannelAddressTest {
+
+ @Test
+ public void test() {
+ ChannelAddress ess0ActivePower = new ChannelAddress("ess0", "ActivePower");
+ ChannelAddress ess0ReactivePower = new ChannelAddress("ess0", "ReactivePower");
+ ChannelAddress meter0ActivePower = new ChannelAddress("meter0", "ActivePower");
+ ChannelAddress meter1ActivePower = new ChannelAddress("meter1", "ActivePower");
+ ChannelAddress meter1ReactivePower = new ChannelAddress("meter1", "ReactivePower");
+ ChannelAddress anyActivePower = new ChannelAddress("*", "ActivePower");
+ ChannelAddress anyMeterActivePower = new ChannelAddress("meter*", "ActivePower");
+ ChannelAddress anyPower = new ChannelAddress("*", "*Power");
+
+ assertEquals(0, ChannelAddress.match(ess0ActivePower, ess0ActivePower));
+ assertEquals(-1, ChannelAddress.match(ess0ActivePower, ess0ReactivePower));
+ assertEquals(Integer.MAX_VALUE / 2 + "*".length(), ChannelAddress.match(ess0ActivePower, anyActivePower));
+ assertEquals(Integer.MAX_VALUE / 2 + "meter*".length(),
+ ChannelAddress.match(meter0ActivePower, anyMeterActivePower));
+ assertEquals(Integer.MAX_VALUE / 2 + "meter*".length(),
+ ChannelAddress.match(meter1ActivePower, anyMeterActivePower));
+ assertEquals("*".length() + "*Power".length(), ChannelAddress.match(meter1ActivePower, anyPower));
+ assertEquals("*".length() + "*Power".length(), ChannelAddress.match(meter1ReactivePower, anyPower));
+ }
+
+}
diff --git a/io.openems.common/test/io/openems/common/utils/StringUtilsTest.java b/io.openems.common/test/io/openems/common/utils/StringUtilsTest.java
index cf596422af2..9aae95441f9 100644
--- a/io.openems.common/test/io/openems/common/utils/StringUtilsTest.java
+++ b/io.openems.common/test/io/openems/common/utils/StringUtilsTest.java
@@ -1,6 +1,6 @@
package io.openems.common.utils;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
import org.junit.Test;
@@ -8,18 +8,18 @@
import com.google.gson.JsonPrimitive;
public class StringUtilsTest {
-
+
@Test
public void testToShortStringStringInt() {
String test = "test to short string";
assertEquals("te...", StringUtils.toShortString(test, 5));
-
+
}
@Test
public void testToShortStringJsonObjectInt() {
JsonObject j = new JsonObject();
- j.add("name", new JsonPrimitive("Testbert")); // {"name":"Testbert"} --> {"name":"T...
+ j.add("name", new JsonPrimitive("Testbert")); // {"name":"Testbert"} --> {"name":"T...
assertEquals("{\"name\":\"T...", StringUtils.toShortString(j, 13));
}
@@ -27,13 +27,26 @@ public void testToShortStringJsonObjectInt() {
public void testCapitalizeFirstLetter() {
assertEquals("Test", StringUtils.capitalizeFirstLetter("test"));
assertEquals("TEST", StringUtils.capitalizeFirstLetter("TEST"));
- assertEquals("1test", StringUtils.capitalizeFirstLetter("1test"));
+ assertEquals("1test", StringUtils.capitalizeFirstLetter("1test"));
}
-
- @Test(expected=IndexOutOfBoundsException.class)
+
+ @Test(expected = IndexOutOfBoundsException.class)
public void testCapitalizeFirstLetterWithEmptyString() {
assertEquals("", StringUtils.capitalizeFirstLetter(""));
-
+ }
+
+ @Test
+ public void testMatchWildcard() {
+ String activePower = "ActivePower";
+ String anyPower = "*Power";
+ String anyActive = "Active*";
+ String any = "*";
+ String foobar = "foobar";
+
+ assertEquals(anyPower.length(), StringUtils.matchWildcard(activePower, anyPower));
+ assertEquals(anyActive.length(), StringUtils.matchWildcard(activePower, anyActive));
+ assertEquals(1, StringUtils.matchWildcard(activePower, any));
+ assertEquals(-1, StringUtils.matchWildcard(activePower, foobar));
}
}
diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun
index ddae1ca8880..6c6e73efb8f 100644
--- a/io.openems.edge.application/EdgeApp.bndrun
+++ b/io.openems.edge.application/EdgeApp.bndrun
@@ -150,7 +150,7 @@
com.google.gson;version='[2.8.5,2.8.6)',\
com.google.guava;version='[29.0.0,29.0.1)',\
com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\
- com.sun.jna;version='[5.6.0,5.6.1)',\
+ com.sun.jna;version='[5.7.0,5.7.1)',\
io.openems.common;version=snapshot,\
io.openems.edge.application;version=snapshot,\
io.openems.edge.battery.api;version=snapshot,\
@@ -300,8 +300,8 @@
org.jsr-305;version='[3.0.2,3.0.3)',\
org.openmuc.jmbus;version='[3.3.0,3.3.1)',\
org.openmuc.jrxtx;version='[1.0.1,1.0.2)',\
- org.ops4j.pax.logging.pax-logging-api;version='[2.0.6,2.0.7)',\
- org.ops4j.pax.logging.pax-logging-log4j1;version='[2.0.6,2.0.7)',\
+ org.ops4j.pax.logging.pax-logging-api;version='[2.0.8,2.0.9)',\
+ org.ops4j.pax.logging.pax-logging-log4j1;version='[2.0.8,2.0.9)',\
org.osgi.util.function;version='[1.1.0,1.1.1)',\
org.osgi.util.promise;version='[1.1.1,1.1.2)',\
rrd4j;version='[3.8.0,3.8.1)'
\ No newline at end of file
diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/api/Battery.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/api/Battery.java
index 200ab163c69..c5d379cc6ec 100644
--- a/io.openems.edge.battery.api/src/io/openems/edge/battery/api/Battery.java
+++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/api/Battery.java
@@ -115,12 +115,9 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
*
+ * The logic uses:
+ *
+ *
+ * This is used as a reference for percentage values in Voltage-To-Percent and
+ * Temperature-To-Percent definitions. If during runtime a higher value is
+ * provided, that one is taken from then on.
+ *
+ * @return the (estimated) maximum expected Charge current in [A]
+ */
+ public int getInitialBmsMaxEverChargeCurrent();
+
+ /**
+ * Defines the (estimated) maximum expected Charge current.
+ *
+ *
+ * This is used as a reference for percentage values in Voltage-To-Percent and
+ * Temperature-To-Percent definitions. If during runtime a higher value is
+ * provided, that one is taken from then on.
+ *
+ * @return the (estimated) maximum expected Charge current in [A]
+ */
+ public int getInitialBmsMaxEverDischargeCurrent();
+
+ /**
+ * Defines the Voltage-to-Percent limits for Charging.
+ *
+ *
+ * Voltage values are in [mV], Percentage in [0,1].
+ *
+ * @return a {@link PolyLine}
+ */
+ public PolyLine getChargeVoltageToPercent();
+
+ /**
+ * Defines the Voltage-to-Percent limits for Discharging.
+ *
+ *
+ * Voltage values are in [mV], Percentage in [0,1].
+ *
+ * @return a {@link PolyLine}
+ */
+ public PolyLine getDischargeVoltageToPercent();
+
+ /**
+ * Defines the Temperature-to-Percent limits for Charging.
+ *
+ *
+ * Temperature values are in [degC], Percentage in [0,1].
+ *
+ * @return a {@link PolyLine}
+ */
+ public PolyLine getChargeTemperatureToPercent();
+
+ /**
+ * Defines the Temperature-to-Percent limits for Discharging.
+ *
+ *
+ * Temperature values are in [degC], Percentage in [0,1].
+ *
+ * @return a {@link PolyLine}
+ */
+ public PolyLine getDischargeTemperatureToPercent();
+
+ /**
+ * Defines the parameters for Force-Discharge mode.
+ *
+ * @return the parameters
+ */
+ public ForceDischarge.Params getForceDischargeParams();
+
+ /**
+ * Defines the parameters for Force-Charge mode.
+ *
+ * @return the ForceChargeParams
+ */
+ public ForceCharge.Params getForceChargeParams();
+
+ /**
+ * Limits the maximum increase in [A] per second. Decrease is never limited for
+ * safety reasons.
+ *
+ * @return the limit or null
+ */
+ public Double getMaxIncreaseAmperePerSecond();
+
+}
diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/AbstractMaxCurrentHandler.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/AbstractMaxCurrentHandler.java
new file mode 100644
index 00000000000..2e5c248d21a
--- /dev/null
+++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/AbstractMaxCurrentHandler.java
@@ -0,0 +1,418 @@
+package io.openems.edge.battery.protection.currenthandler;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.atomic.AtomicReference;
+
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.edge.battery.api.Battery;
+import io.openems.edge.battery.protection.BatteryProtection.ChannelId;
+import io.openems.edge.battery.protection.force.AbstractForceChargeDischarge;
+import io.openems.edge.common.channel.IntegerReadChannel;
+import io.openems.edge.common.component.ClockProvider;
+import io.openems.edge.common.linecharacteristic.PolyLine;
+import io.openems.edge.common.type.TypeUtils;
+
+public abstract class AbstractMaxCurrentHandler {
+
+ public abstract static class Builder
+ * If for the given 'cellVoltage' value 'voltageToPercent' defines a limitation
+ * (i.e. the given percentage is less than 100 %), that limitation stays active
+ * until a future 'cellVoltage' results in no limitation (i.e. percentage == 100
+ * %). This is implemented to reduce fluctuations due to physical effects in the
+ * battery.
+ *
+ *
+ * This method internally uses the abstract
+ * {@link #getActiveCellVoltageToPercentLimit()} method to distinguish between
+ * active charge/discharge limitations.
+ *
+ * @param activeLimit the currently active limit
+ * @param cellVoltage the cell-voltage
+ * @return the Cell-Voltage-To-Percent Limit
+ */
+ private synchronized Double getCellVoltageToPercentLimit(AtomicReference
+ * If maxIncreasePerSecond is 0.5, last limit was 10 A and 1 second passed, this
+ * method returns 10.5.
+ *
+ * @return the limit or null
+ */
+ protected synchronized Double getMaxIncreaseAmpereLimit() {
+ if (this.maxIncreasePerSecond == null) {
+ return null;
+ }
+ Instant now = Instant.now(this.clockProvider.getClock());
+ final Double result;
+ if (this.lastResultTimestamp != null && this.lastCurrentLimit != null) {
+ result = this.lastCurrentLimit
+ + (Duration.between(this.lastResultTimestamp, now).toMillis() * this.maxIncreasePerSecond) //
+ / 1000.; // convert [mA] to [A]
+ } else {
+ result = 0.;
+ }
+ this.lastResultTimestamp = now;
+ return result;
+ }
+
+ /**
+ * Calculates the Ampere limit in Force Charge/Discharge mode. Returns:
+ *
+ *
*
- * To receive events, an object must implement the
- *
+ * To receive events, an object must implement the
+ *
- *
*
*
+ *
+ */
+public class BatteryProtection {
+
+ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
+ /**
+ * Charge Current limit provided by the Battery/BMS.
+ *
+ *
+ *
+ */
+ BP_CHARGE_BMS(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Discharge Current limit provided by the Battery/BMS.
+ *
+ *
+ *
+ */
+ BP_DISCHARGE_BMS(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Charge Current limit derived from Min-Cell-Voltage.
+ *
+ *
+ *
+ */
+ BP_CHARGE_MIN_VOLTAGE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Discharge Current limit derived from Min-Cell-Voltage.
+ *
+ *
+ *
+ */
+ BP_DISCHARGE_MIN_VOLTAGE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Charge Current limit derived from Max-Cell-Voltage.
+ *
+ *
+ *
+ */
+ BP_CHARGE_MAX_VOLTAGE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Discharge Current limit derived from Max-Cell-Voltage.
+ *
+ *
+ *
+ */
+ BP_DISCHARGE_MAX_VOLTAGE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Charge Current limit derived from Min-Cell-Temperature.
+ *
+ *
+ *
+ */
+ BP_CHARGE_MIN_TEMPERATURE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Discharge Current limit derived from Min-Cell-Temperature.
+ *
+ *
+ *
+ */
+ BP_DISCHARGE_MIN_TEMPERATURE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Charge Current limit derived from Max-Cell-Temperature.
+ *
+ *
+ *
+ */
+ BP_CHARGE_MAX_TEMPERATURE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Discharge Current limit derived from Max-Cell-Temperature.
+ *
+ *
+ *
+ */
+ BP_DISCHARGE_MAX_TEMPERATURE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Charge Max-Increase Current limit.
+ *
+ *
+ *
+ */
+ BP_CHARGE_INCREASE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Discharge Max-Increase Current limit.
+ *
+ *
+ *
+ */
+ BP_DISCHARGE_INCREASE(Doc.of(OpenemsType.INTEGER) //
+ .unit(Unit.AMPERE)),
+ /**
+ * Force-Discharge State.
+ *
+ *
+ *
+ */
+ BP_FORCE_DISCHARGE(Doc.of(AbstractForceChargeDischarge.State.values())), //
+ /**
+ * Force-Charge State.
+ *
+ *
+ *
+ */
+ BP_FORCE_CHARGE(Doc.of(AbstractForceChargeDischarge.State.values())) //
+ ;
+
+ private final Doc doc;
+
+ private ChannelId(Doc doc) {
+ this.doc = doc;
+ }
+
+ @Override
+ public Doc doc() {
+ return this.doc;
+ }
+ }
+
+ public static class Builder {
+
+ private final Battery battery;
+
+ private ChargeMaxCurrentHandler chargeMaxCurrentHandler;
+ private DischargeMaxCurrentHandler dischargeMaxCurrentHandler;
+
+ protected Builder(Battery battery) {
+ this.battery = battery;
+ }
+
+ /**
+ * Applies all values from a {@link BatteryProtectionDefinition}.
+ *
+ * @param def the {@link BatteryProtectionDefinition}
+ * @param clockProvider a {@link ClockProvider}
+ * @return a {@link Builder}
+ */
+ public Builder applyBatteryProtectionDefinition(BatteryProtectionDefinition def, ClockProvider clockProvider) {
+ return this //
+ .setChargeMaxCurrentHandler(
+ ChargeMaxCurrentHandler.create(clockProvider, def.getInitialBmsMaxEverChargeCurrent()) //
+ .setVoltageToPercent(def.getChargeVoltageToPercent()) //
+ .setTemperatureToPercent(def.getChargeTemperatureToPercent()) //
+ .setMaxIncreasePerSecond(def.getMaxIncreaseAmperePerSecond()) //
+ .setForceDischarge(def.getForceDischargeParams()) //
+ .build()) //
+ .setDischargeMaxCurrentHandler(
+ DischargeMaxCurrentHandler.create(clockProvider, def.getInitialBmsMaxEverDischargeCurrent()) //
+ .setVoltageToPercent(def.getDischargeVoltageToPercent())
+ .setTemperatureToPercent(def.getDischargeTemperatureToPercent()) //
+ .setMaxIncreasePerSecond(def.getMaxIncreaseAmperePerSecond()) //
+ .setForceCharge(def.getForceChargeParams()) //
+ .build()) //
+ ;
+ }
+
+ /**
+ * Sets the {@link ChargeMaxCurrentHandler}.
+ *
+ * @param chargeMaxCurrentHandler the {@link ChargeMaxCurrentHandler}
+ * @return a {@link Builder}
+ */
+ public Builder setChargeMaxCurrentHandler(ChargeMaxCurrentHandler chargeMaxCurrentHandler) {
+ this.chargeMaxCurrentHandler = chargeMaxCurrentHandler;
+ return this;
+ }
+
+ /**
+ * Sets the {@link DischargeMaxCurrentHandler}.
+ *
+ * @param dischargeMaxCurrentHandler the {@link DischargeMaxCurrentHandler}
+ * @return a {@link Builder}
+ */
+ public Builder setDischargeMaxCurrentHandler(DischargeMaxCurrentHandler dischargeMaxCurrentHandler) {
+ this.dischargeMaxCurrentHandler = dischargeMaxCurrentHandler;
+ return this;
+ }
+
+ /**
+ * Builds the {@link BatteryProtection} instance.
+ *
+ * @return a {@link BatteryProtection}
+ */
+ public BatteryProtection build() {
+ return new BatteryProtection(this.battery, this.chargeMaxCurrentHandler, this.dischargeMaxCurrentHandler);
+ }
+ }
+
+ /**
+ * Create a {@link BatteryProtection} using builder pattern.
+ *
+ * @param battery the {@link Battery}
+ * @return a {@link Builder}
+ */
+ public static Builder create(Battery battery) {
+ return new Builder(battery);
+ }
+
+ private final Battery battery;
+ private final ChargeMaxCurrentHandler chargeMaxCurrentHandler;
+ private final DischargeMaxCurrentHandler dischargeMaxCurrentHandler;
+
+ protected BatteryProtection(Battery battery, ChargeMaxCurrentHandler chargeMaxCurrentHandler,
+ DischargeMaxCurrentHandler dischargeMaxCurrentHandler) {
+ TypeUtils.assertNull("BatteryProtection algorithm is missing data", battery, chargeMaxCurrentHandler,
+ dischargeMaxCurrentHandler);
+ this.battery = battery;
+ this.chargeMaxCurrentHandler = chargeMaxCurrentHandler;
+ this.dischargeMaxCurrentHandler = dischargeMaxCurrentHandler;
+ }
+
+ /**
+ * Apply the logic on the {@link Battery}.
+ *
+ *
+ *
+ */
+ public void apply() {
+ // Use MaxCurrentHandlers to calculate max charge and discharge currents.
+ // These methods also write debug information to BatteryProtection-Channels, so
+ // it is feasible to always execute them, even if battery is not started.
+ int chargeMaxCurrent = this.chargeMaxCurrentHandler.calculateCurrentLimit(this.battery);
+ int dischargeMaxCurrent = this.dischargeMaxCurrentHandler.calculateCurrentLimit(this.battery);
+
+ // Set max charge and discharge currents
+ this.battery._setChargeMaxCurrent(chargeMaxCurrent);
+ this.battery._setDischargeMaxCurrent(dischargeMaxCurrent);
+ }
+}
diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/BatteryProtectionDefinition.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/BatteryProtectionDefinition.java
new file mode 100644
index 00000000000..fa85683afff
--- /dev/null
+++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/BatteryProtectionDefinition.java
@@ -0,0 +1,95 @@
+package io.openems.edge.battery.protection;
+
+import io.openems.edge.battery.protection.force.ForceCharge;
+import io.openems.edge.battery.protection.force.ForceDischarge;
+import io.openems.edge.common.linecharacteristic.PolyLine;
+
+public interface BatteryProtectionDefinition {
+
+ /**
+ * Defines the (estimated) maximum expected Charge current.
+ *
+ *
+ *
+ *
+ * @return the {@link ChannelId}
+ */
+ protected abstract ChannelId getBpBmsChannelId();
+
+ /**
+ * Gets the ChannelId for Battery-Protection Limit by Min-Cell-Voltage.
+ *
+ *
+ *
+ *
+ * @return the {@link ChannelId}
+ */
+ protected abstract ChannelId getBpMinVoltageChannelId();
+
+ /**
+ * Gets the ChannelId for Battery-Protection Limit by Max-Cell-Voltage.
+ *
+ *
+ *
+ *
+ * @return the {@link ChannelId}
+ */
+ protected abstract ChannelId getBpMaxVoltageChannelId();
+
+ /**
+ * Gets the ChannelId for Battery-Protection Limit by Min-Cell-Temperature.
+ *
+ *
+ *
+ *
+ * @return the {@link ChannelId}
+ */
+ protected abstract ChannelId getBpMinTemperatureChannelId();
+
+ /**
+ * Gets the ChannelId for Battery-Protection Limit by Max-Cell-Temperature.
+ *
+ *
+ *
+ *
+ * @return the {@link ChannelId}
+ */
+ protected abstract ChannelId getBpMaxTemperatureChannelId();
+
+ /**
+ * Gets the ChannelId for Battery-Protection Limit by Force Charge/Discharge
+ * Mode.
+ *
+ *
+ *
+ *
+ * @return the {@link ChannelId}
+ */
+ protected abstract ChannelId getBpForceCurrentChannelId();
+
+ /**
+ * Gets the ChannelId for Battery-Protection Limit by Max-Increase-Ampere ramp.
+ * Mode.
+ *
+ *
+ *
+ *
+ * @return the {@link ChannelId}
+ */
+ protected abstract ChannelId getBpMaxIncreaseAmpereChannelId();
+
+ /**
+ * Calculates the actual allowed current limit in [A] as minimum of:.
+ *
+ *
+ *
+ *
+ * @param battery the {@link Battery}
+ * @return the actual allowed current limit, mathematically rounded to [A]
+ */
+ public synchronized int calculateCurrentLimit(Battery battery) {
+ // Read input parameters from Battery
+ Integer minCellVoltage = battery.getMinCellVoltage().get();
+ Integer maxCellVoltage = battery.getMaxCellVoltage().get();
+ Integer minCellTemperature = battery.getMinCellTemperature().get();
+ Integer maxCellTemperature = battery.getMaxCellTemperature().get();
+ IntegerReadChannel bpBmsChannel = battery.channel(this.getBpBmsChannelId());
+ Integer bpBms = bpBmsChannel.value().get();
+
+ // Update 'bmsMaxEverAllowedCurrent'
+ this.bmsMaxEverCurrent = TypeUtils.max(this.bmsMaxEverCurrent, bpBms);
+
+ /*
+ * Get all limits
+ */
+ // Calculate Ampere limit for Min-Cell-Voltage
+ final Double minCellVoltageLimit = this.getMinCellVoltageToPercentLimit(minCellVoltage);
+ // Calculate Ampere limit for Max-Cell-Voltage
+ final Double maxCellVoltageLimit = this.getMaxCellVoltageToPercentLimit(maxCellVoltage);
+ // Calculate Ampere limit for Min-Cell-Temperature
+ final Double minCellTemperatureLimit = this
+ .percentToAmpere(this.temperatureToPercent.getValue(minCellTemperature));
+ // Calculate Ampere limit for Max-Cell-Temperature
+ final Double maxCellTemperatureLimit = this
+ .percentToAmpere(this.temperatureToPercent.getValue(maxCellTemperature));
+ // Calculate Max Increase Ampere Limit
+ final Double maxIncreaseAmpereLimit = this.getMaxIncreaseAmpereLimit();
+ // Calculate Force Current
+ final Double forceCurrent = this.getForceCurrent(minCellVoltage, maxCellVoltage);
+
+ /*
+ * Store limits in Channels
+ */
+ battery.channel(this.getBpMinVoltageChannelId()).setNextValue(minCellVoltageLimit);
+ battery.channel(this.getBpMaxVoltageChannelId()).setNextValue(maxCellVoltageLimit);
+ battery.channel(this.getBpMinTemperatureChannelId()).setNextValue(minCellTemperatureLimit);
+ battery.channel(this.getBpMaxTemperatureChannelId()).setNextValue(maxCellTemperatureLimit);
+ battery.channel(this.getBpMaxIncreaseAmpereChannelId()).setNextValue(maxIncreaseAmpereLimit);
+ battery.channel(this.getBpForceCurrentChannelId()).setNextValue(forceCurrent);
+
+ // Get the minimum limit of all limits in Ampere
+ Double limit = TypeUtils.min(TypeUtils.toDouble(bpBms), minCellVoltageLimit, maxCellVoltageLimit,
+ minCellTemperatureLimit, maxCellTemperatureLimit, maxIncreaseAmpereLimit, forceCurrent);
+
+ // Battery not started? Set '0' to block charge/discharge
+ if (!battery.isStarted()) {
+ limit = 0.;
+ }
+
+ // No limit? Set '0' to block charge/discharge
+ if (limit == null) {
+ limit = 0.;
+ }
+
+ this.lastCurrentLimit = limit;
+
+ return (int) Math.round(limit);
+ }
+
+ /**
+ * Calculates the current limit based on Min-/Max-Cell-Voltage according to the
+ * 'voltageToPercent' characteristics.
+ *
+ *
+ *
+ *
+ * @param minCellVoltage the Min-Cell-Voltage, possibly null
+ * @param maxCellVoltage the Max-Cell-Voltage, possibly null
+ * @return the Current, possibly null
+ */
+ protected Double getForceCurrent(Integer minCellVoltage, Integer maxCellVoltage) {
+ if (this.forceChargeDischarge == null) {
+ return null;
+ }
+
+ final AbstractForceChargeDischarge.State state;
+
+ // Apply State-Machine
+ if (minCellVoltage == null || maxCellVoltage == null) {
+ this.forceChargeDischarge.forceNextState(AbstractForceChargeDischarge.State.UNDEFINED);
+ state = AbstractForceChargeDischarge.State.UNDEFINED;
+
+ } else {
+ try {
+ this.forceChargeDischarge.run(
+ new AbstractForceChargeDischarge.Context(this.clockProvider, minCellVoltage, maxCellVoltage));
+ } catch (OpenemsNamedException e) {
+ e.printStackTrace();
+ }
+ state = this.forceChargeDischarge.getCurrentState();
+ }
+
+ // Evaluate force charge/discharge current from current state
+ switch (state) {
+ case UNDEFINED:
+ case WAIT_FOR_FORCE_MODE:
+ return null;
+ case FORCE_MODE:
+ return -1.;
+ case BLOCK_MODE:
+ return 0.;
+ }
+ // will never happen
+ return null;
+ }
+
+ /**
+ * Convert a Percent value to a concrete Ampere value in [A] by multiplying it
+ * with 'bmsMaxEverAllowedChargeCurrent'.
+ *
+ *
+ *
+ *
+ * @param percent the percent value in [0,1]
+ * @return the ampere value in [A]
+ */
+ protected Double percentToAmpere(Double percent) {
+ if (percent == null) {
+ return null;
+ } else if (percent == 0.) {
+ return 0.;
+ } else {
+ return Math.max(1., this.bmsMaxEverCurrent * percent);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/ChargeMaxCurrentHandler.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/ChargeMaxCurrentHandler.java
new file mode 100644
index 00000000000..129a7c28c6e
--- /dev/null
+++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/ChargeMaxCurrentHandler.java
@@ -0,0 +1,136 @@
+package io.openems.edge.battery.protection.currenthandler;
+
+import io.openems.edge.battery.protection.BatteryProtection;
+import io.openems.edge.battery.protection.BatteryProtection.ChannelId;
+import io.openems.edge.battery.protection.force.ForceDischarge;
+import io.openems.edge.common.component.ClockProvider;
+import io.openems.edge.common.linecharacteristic.PolyLine;
+
+public class ChargeMaxCurrentHandler extends AbstractMaxCurrentHandler {
+
+ public static class Builder extends AbstractMaxCurrentHandler.Builder
*
*
*
*
*
- * @throws FileNotFoundException when the file/directory path is invalid or
- * there was an IOException thrown when trying to
- * read the device.
+ * @throws OWFileNotFoundException when the file/directory path is invalid or
+ * there was an IOException thrown when trying
+ * to read the device.
*/
protected void open() throws OWFileNotFoundException {
String last_error = null;
@@ -600,18 +600,18 @@ protected void close() throws IOException {
/**
* Creates a directory or file to write.
*
- * @param append for files only, true to append data to end of file, false
- * to reset the file
- * @param isDirectory true if creating a directory, false for a file
- * @param makeParents true if creating all needed parent directories in order
- * to create the file/directory
- * @param startPageNum starting page of file/directory, -1 if not renaming
- * @param numberPages number of pages in file/directory, -1 if not renaming
- *
- * @throws FileNotFoundException if file already opened to write, if
- * makeParents=false and parent directories not
- * found, if file is read only, or if there is an
- * IO error reading filesystem
+ * @param append for files only, true to append data to end of file, false
+ * to reset the file
+ * @param isDirectory true if creating a directory, false for a file
+ * @param makeParents true if creating all needed parent directories in order to
+ * create the file/directory
+ * @param startPage starting page of file/directory, -1 if not renaming
+ * @param numberPages number of pages in file/directory, -1 if not renaming
+ *
+ * @throws OWFileNotFoundException if file already opened to write, if
+ * makeParents=false and parent directories not
+ * found, if file is read only, or if there is
+ * an IO error reading filesystem
*/
protected void create(boolean append, boolean isDirectory, boolean makeParents, int startPage, int numberPages)
throws OWFileNotFoundException {
@@ -783,7 +783,8 @@ protected void create(boolean append, boolean isDirectory, boolean makeParents,
* WARNING: all files/directories will be deleted in the process.
*
* @throws OneWireException when adapter is not setup properly
- * @throws OneWireIOException when an IO error occurred reading the 1-Wire device
+ * @throws OneWireIOException when an IO error occurred reading the 1-Wire
+ * device
*/
protected void format() throws OneWireException, OneWireIOException {
int i, j, len, next_page, cnt, cdcnt = 0, device_map_pages, dm_bytes = 0;
@@ -2853,17 +2854,17 @@ private void validateFileSystem() throws OneWireException {
/**
* Verify the Device Map of a MASTER device is correct.
*
- * @param page starting page number of the device map file
+ * @param startPage starting page number of the device map file
* @param numberOfContainers to re-create the OneWireContainer array in the
* instance variable from the devices listed in the
* device map 'owd[]'. Zero indicates leave the list
- * alone. >0 means recreate the array keeping the same
- * MASTER device.
+ * alone. >0 means recreate the array keeping the
+ * same MASTER device.
* @param setOverdrive
*
diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFile.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFile.java
index 255f1ebd636..19b48462f11 100644
--- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFile.java
+++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFile.java
@@ -377,7 +377,6 @@ public OWFile(OneWireContainer owd, String parent, String child) {
* abstract pathname and the child abstract pathname is resolved against the
* parent.
*
- * @param owd OneWireContainer that this Filesystem resides on
* @param parent The parent abstract pathname
* @param child The child pathname string
* @throws NullPointerException If child
is null
@@ -957,8 +956,7 @@ public int compareTo(OWFile pathname) {
* value greater than zero if this abstract pathname is
* lexicographically greater than the argument
*
- * @throws ClassCastException
if the argument is not an abstract
- * pathname
+ * @throws ClassCastException if the argument is not an abstract pathname
*
* @see java.lang.Comparable
*/
@@ -995,8 +993,8 @@ public boolean equals(Object obj) {
* codes. On UNIX systems, the hash code of an abstract pathname is equal to the
* exclusive or of its pathname string and the decimal value
* 1234321
. On Win32 systems, the hash code is equal to the
- * exclusive or of its pathname string, converted to lower case, and the
- * decimal value 1234321
.
+ * exclusive or of its pathname string, converted to lower case, and
+ * the decimal value 1234321
.
*
* @return A hash code for this abstract pathname
*/
diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileDescriptor.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileDescriptor.java
index 60aad403a79..6281fdc25de 100644
--- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileDescriptor.java
+++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileDescriptor.java
@@ -504,9 +504,9 @@ public void sync() throws OWSyncFailedException {
* true
if set new containers to do a
* max speed of overdrive if possible
*
- * @returns the number of devices in the device map if the current device list
- * is INVALID and returns zero if the current device list is VALID.
+ * @return the number of devices in the device map if the current device list is
+ * INVALID and returns zero if the current device list is VALID.
*
* @throws OneWireException when an IO error occurs
*/
@@ -3116,8 +3117,8 @@ protected boolean createNewFile() throws IOException {
* codes. On UNIX systems, the hash code of an abstract pathname is equal to the
* exclusive or of its pathname string and the decimal value
* 1234321
. On Win32 systems, the hash code is equal to the
- * exclusive or of its pathname string, converted to lower case, and the
- * decimal value 1234321
.
+ * exclusive or of its pathname string, converted to lower case, and
+ * the decimal value 1234321
.
*
* @return A hash code for this abstract pathname
*/
diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileInputStream.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileInputStream.java
index f0fb2aa7fca..b7ff339cfd3 100644
--- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileInputStream.java
+++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileInputStream.java
@@ -28,7 +28,6 @@
package com.dalsemi.onewire.application.file;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -61,8 +60,6 @@
* Usage
- *
- *
Example
Read from a 1-Wire file on device 'owd':
*
*
@@ -116,9 +113,9 @@ public class OWFileInputStream extends InputStream {
*
* @param owd OneWireContainer that this Filesystem resides on
* @param name the system-dependent file name.
- * @exception FileNotFoundException if the file does not exist, is a directory
- * rather than a regular file, or for some
- * other reason cannot be opened for reading.
+ * @exception OWFileNotFoundException if the file does not exist, is a directory
+ * rather than a regular file, or for some
+ * other reason cannot be opened for reading.
*/
public OWFileInputStream(OneWireContainer owd, String name) throws OWFileNotFoundException {
fd = new OWFileDescriptor(owd, name);
@@ -155,9 +152,9 @@ public OWFileInputStream(OneWireContainer owd, String name) throws OWFileNotFoun
*
* @param owd array of OneWireContainers that this Filesystem resides on
* @param name the system-dependent file name.
- * @exception FileNotFoundException if the file does not exist, is a directory
- * rather than a regular file, or for some
- * other reason cannot be opened for reading.
+ * @exception OWFileNotFoundException if the file does not exist, is a directory
+ * rather than a regular file, or for some
+ * other reason cannot be opened for reading.
*/
public OWFileInputStream(OneWireContainer[] owd, String name) throws OWFileNotFoundException {
fd = new OWFileDescriptor(owd, name);
@@ -190,9 +187,9 @@ public OWFileInputStream(OneWireContainer[] owd, String name) throws OWFileNotFo
*
FileNotFoundException
is thrown.
*
* @param file the file to be opened for reading.
- * @exception FileNotFoundException if the file does not exist, is a directory
- * rather than a regular file, or for some
- * other reason cannot be opened for reading.
+ * @exception OWFileNotFoundException if the file does not exist, is a directory
+ * rather than a regular file, or for some
+ * other reason cannot be opened for reading.
* @see com.dalsemi.onewire.application.file.OWFile#getPath()
*/
public OWFileInputStream(OWFile file) throws OWFileNotFoundException {
diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileOutputStream.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileOutputStream.java
index 204ddef097b..61385a67e50 100644
--- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileOutputStream.java
+++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileOutputStream.java
@@ -28,7 +28,6 @@
package com.dalsemi.onewire.application.file;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
@@ -80,8 +79,6 @@
*
*
* Usage
- *
- *
Example
Write to a 1-Wire file on device 'owd':
*
*
@@ -152,13 +149,13 @@ public class OWFileOutputStream extends OutputStream {
*
* @param owd OneWireContainer that this Filesystem resides on
* @param name the system-dependent filename
- * @exception FileNotFoundException if the file exists but is a directory rather
- * than a regular file, does not exist but
- * cannot be created, or cannot be opened for
- * any other reason
- * @exception SecurityException if a security manager exists and its
- *
checkWrite
method denies write
- * access to the file.
+ * @exception OWFileNotFoundException if the file exists but is a directory
+ * rather than a regular file, does not exist
+ * but cannot be created, or cannot be opened
+ * for any other reason
+ * @exception SecurityException if a security manager exists and its
+ * checkWrite
method denies
+ * write access to the file.
*/
public OWFileOutputStream(OneWireContainer owd, String name) throws OWFileNotFoundException {
OneWireContainer[] devices = new OneWireContainer[1];
@@ -188,13 +185,10 @@ public OWFileOutputStream(OneWireContainer owd, String name) throws OWFileNotFou
*
* @param owd array of OneWireContainers that this Filesystem resides on
* @param name the system-dependent filename
- * @exception FileNotFoundException if the file exists but is a directory rather
- * than a regular file, does not exist but
- * cannot be created, or cannot be opened for
- * any other reason
- * @exception SecurityException if a security manager exists and its
- * checkWrite
method denies write
- * access to the file.
+ * @exception OWFileNotFoundException if the file exists but is a directory
+ * rather than a regular file, does not exist
+ * but cannot be created, or cannot be opened
+ * for any other reason
*/
public OWFileOutputStream(OneWireContainer[] owd, String name) throws OWFileNotFoundException {
fd = new OWFileDescriptor(owd, name);
@@ -226,13 +220,13 @@ public OWFileOutputStream(OneWireContainer[] owd, String name) throws OWFileNotF
* @param name the system-dependent file name
* @param append if true
, then bytes will be written to the end of
* the file rather than the beginning
- * @exception FileNotFoundException if the file exists but is a directory rather
- * than a regular file, does not exist but
- * cannot be created, or cannot be opened for
- * any other reason.
- * @exception SecurityException if a security manager exists and its
- * checkWrite
method denies write
- * access to the file.
+ * @exception OWFileNotFoundException if the file exists but is a directory
+ * rather than a regular file, does not exist
+ * but cannot be created, or cannot be opened
+ * for any other reason.
+ * @exception SecurityException if a security manager exists and its
+ * checkWrite
method denies
+ * write access to the file.
*/
public OWFileOutputStream(OneWireContainer owd, String name, boolean append) throws OWFileNotFoundException {
fd = new OWFileDescriptor(owd, name);
@@ -264,13 +258,13 @@ public OWFileOutputStream(OneWireContainer owd, String name, boolean append) thr
* @param name the system-dependent file name
* @param append if true
, then bytes will be written to the end of
* the file rather than the beginning
- * @exception FileNotFoundException if the file exists but is a directory rather
- * than a regular file, does not exist but
- * cannot be created, or cannot be opened for
- * any other reason.
- * @exception SecurityException if a security manager exists and its
- * checkWrite
method denies write
- * access to the file.
+ * @exception OWFileNotFoundException if the file exists but is a directory
+ * rather than a regular file, does not exist
+ * but cannot be created, or cannot be opened
+ * for any other reason.
+ * @exception SecurityException if a security manager exists and its
+ * checkWrite
method denies
+ * write access to the file.
*/
public OWFileOutputStream(OneWireContainer[] owd, String name, boolean append) throws OWFileNotFoundException {
fd = new OWFileDescriptor(owd, name);
@@ -298,13 +292,13 @@ public OWFileOutputStream(OneWireContainer[] owd, String name, boolean append) t
* FileNotFoundException
is thrown.
*
* @param file the file to be opened for writing.
- * @exception FileNotFoundException if the file exists but is a directory rather
- * than a regular file, does not exist but
- * cannot be created, or cannot be opened for
- * any other reason
- * @exception SecurityException if a security manager exists and its
- * checkWrite
method denies write
- * access to the file.
+ * @exception OWFileNotFoundException if the file exists but is a directory
+ * rather than a regular file, does not exist
+ * but cannot be created, or cannot be opened
+ * for any other reason
+ * @exception SecurityException if a security manager exists and its
+ * checkWrite
method denies
+ * write access to the file.
* @see java.io.File#getPath()
*/
public OWFileOutputStream(OWFile file) throws OWFileNotFoundException {
diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/AbstractDeviceMonitor.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/AbstractDeviceMonitor.java
index 62e86572948..2e75cc1cc84 100644
--- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/AbstractDeviceMonitor.java
+++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/AbstractDeviceMonitor.java
@@ -66,18 +66,13 @@
* environment. Instead of reporting the exception on each failed search
* attempt, the monitor will default to retrying the search a handful of times
* before finally reporting the exception.
- *
- * @see #getMaxErrorCount(). To disable this feature, set the max error count to
- * 1.
- * @see #setMaxErrorCount(int).
- * DeviceMonitorEventListener
interface.
- * @see DeviceMonitorEventListener. And the object must be added to the list of
- * listeners. @see #addDeviceMonitorEventListener.
- * DeviceMonitorEventListener
interface. And the object must be
+ * added to the list of listeners. @see #addDeviceMonitorEventListener.
+ * Usage
*
- *
*
*
- *
- * byte[] buffer = {(byte)0x90, (byte)0x00, (byte)0x00, (byte)0x00,
- * (byte)0x01, (byte)0x02, (byte)0x03};
- * CommandAPDU capdu = new CommandAPDU(buffer);
+ *
+ *
- * CommandAPDU capdu = new CommandAPDU((byte)0x90, (byte)0x00, (byte)0x00, (byte)0x00,
- * (byte)0x01, (byte)0x02, (byte)0x03);
+ * byte[] buffer = { (byte) 0x90, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03 };
+ * CommandAPDU capdu = new CommandAPDU(buffer);
+ *
+ *
+ *
+ * CommandAPDU capdu = new CommandAPDU((byte) 0x90, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02,
+ * (byte) 0x03);
+ *
* Additional information
@@ -72,8 +75,6 @@
*
*
* @see com.dalsemi.onewire.container.ResponseAPDU
- * @see com.dalsemi.onewire.container.OneWireContainer16
- *
* @version 0.00, 28 Aug 2000
* @author YL
*
diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/HumidityContainer.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/HumidityContainer.java
index 226e1159c86..7db95ba9246 100644
--- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/HumidityContainer.java
+++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/HumidityContainer.java
@@ -67,8 +67,6 @@
*
* Usage
*
- *
- *
Example
Gets humidity reading from a HumidityContainer instance
* 'hc
':
*
diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBank.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBank.java
index 44af2566397..42db04c135c 100644
--- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBank.java
+++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBank.java
@@ -95,13 +95,11 @@
* with zeros:
*
*
- *
*
*
- * byte[] write_buf = new byte[mb.getSize()];
- * for (int i = 0; i < write_buf.length; i++)
- * write_buf[i] = (byte)0;
+ * byte[] write_buf = new byte[mb.getSize()];
+ * for (int i = 0; i > write_buf.length; i++)
+ * write_buf[i] = (byte) 0;
*
- * mb.write(0, write_buf, 0, write_buf.length);
- *
+ * mb.write(0, write_buf, 0, write_buf.length);
* Features
*
*
*
* Alternate Names
diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer02.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer02.java
index 42152f23c81..3b16fd9ec6e 100644
--- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer02.java
+++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer02.java
@@ -51,8 +51,7 @@
* readDevice()
*
- * @return time in milliseconds that have occurred since the interval counter was
- * started
+ * @return time in milliseconds that have occurred since the interval counter
+ * was started
*
* @see com.dalsemi.onewire.container.OneWireSensor#readDevice()
* @see #setIntervalTimer(long,byte[])
diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer05.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer05.java
index 4e4ca37b60a..8ceb87bce0b 100644
--- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer05.java
+++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer05.java
@@ -43,8 +43,7 @@
*
*
@@ -64,36 +63,35 @@
* level of the PIO pin, then in the loop it toggles the latch state.
*
- * // "ID" is a byte array of size 8 with an address of a part we
- * // have already found with family code 05 hex
- * // "access" is a DSPortAdapter
+ *
+ * // "ID" is a byte array of size 8 with an address of a part we
+ * // have already found with family code 05 hex
+ * // "access" is a DSPortAdapter
*
- * int i=0;
- * OneWireContainer05 ds2405 = (OneWireContainer05) access.getDeviceContainer(ID);
- * ds2405.setupContainer(access,ID);
+ * int i = 0;
+ * OneWireContainer05 ds2405 = (OneWireContainer05) access.getDeviceContainer(ID);
+ * ds2405.setupContainer(access, ID);
*
- * byte[] state = ds2405.readDevice();
+ * byte[] state = ds2405.readDevice();
*
- * // I know that the 2405 only has one channel (one switch)
- * // and it doesn't support 'Smart On'
+ * // I know that the 2405 only has one channel (one switch)
+ * // and it doesn't support 'Smart On'
*
- * boolean latch_state = ds2405.getLatchState(0,state);
- * System.out.println("Current state of switch: "+latch_state);
- * System.out.println("Current output level: "+ds2405.getLevel(0,state));
- * while (++i < 100)
- * {
- * System.out.println("Toggling switch");
- * ds2405.setLatchState(0,!latch_state,false,state);
- * ds2405.writeDevice(state);
- * state = ds2405.readDevice();
- * latch_state = ds2405.getLatchState(0,state);
- * System.out.println("Current state of switch: "+latch_state);
- * System.out.println("Current output level: "+ds2405.getLevel(0,state));
- * Thread.sleep(500);
- * }
+ * boolean latch_state = ds2405.getLatchState(0, state);
+ * System.out.println("Current state of switch: " + latch_state);
+ * System.out.println("Current output level: " + ds2405.getLevel(0, state));
+ * while (++i < 100) {
+ * System.out.println("Toggling switch");
+ * ds2405.setLatchState(0, !latch_state, false, state);
+ * ds2405.writeDevice(state);
+ * state = ds2405.readDevice();
+ * latch_state = ds2405.getLatchState(0, state);
+ * System.out.println("Current state of switch: " + latch_state);
+ * System.out.println("Current output level: " + ds2405.getLevel(0, state));
+ * Thread.sleep(500);
+ * }
*
- *
+ *
*
* * Also see the usage example in the diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer06.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer06.java index 7a629ce1b1b..72b69a9c10f 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer06.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer06.java @@ -50,8 +50,7 @@ *
readAuthenticatedPage
}
* writeDataPage
}
*
- *
*
*
* The memory can also be accessed through the objects that are returned from
@@ -797,55 +796,56 @@ public synchronized boolean eraseScratchPad(int page) throws OneWireIOException,
* the DS1963S to stop sending 0x0ff's and begin alternating its output bits.
* This method reads until it finds a non-0x0ff byte
or until it
* reaches a specified number of tries, indicating failure.
- *
* This method can often be optimized away. A normal 1-Wire transaction involves
* writing and reading a known number of bytes. If a few more bytes are read, a
* program can check to see if the DS1963S has started alternating its output
* much quicker than calling this method will. For instance, to copy the
- * scratchpad, the source code might look like this:
To optimize the code, read more bytes than required:
- *
- * buffer [0] = COPY_SCRATCHPAD;
- * buffer [1] = TA1;
- * buffer [2] = TA2;
- * buffer [3] = ES;
- *
- * adapter.dataBlock(buffer,0,4);
- * return waitForSuccessfulFinish();
- *
- *
- * buffer [0] = COPY_SCRATCHPAD;
- * buffer [1] = TA1;
- * buffer [2] = TA2;
- * buffer [3] = ES;
- *
- * //copy 0x0FF into the buffer, this effectively reads
- * System.arraycopy(FF, 0, buffer, 4, 5);
- *
- * //read 5 extra bytes
- * adapter.dataBlock(buffer, 0, 9);
- *
- * //if the last byte has not shown alternating output,
- * //still call waitForSuccessfulFinish(), else
- * //we are already done
- * if (buffer [8] == ( byte ) 0x0ff)
- * return waitForSuccessfulFinish();
- * else
- * return true;
- *
+ * buffer[0] = COPY_SCRATCHPAD; + * buffer[1] = TA1; + * buffer[2] = TA2; + * buffer[3] = ES; + * + * adapter.dataBlock(buffer, 0, 4); + * return waitForSuccessfulFinish(); + *+ * + * To optimize the code, read more bytes than required: + * + *
+ * buffer[0] = COPY_SCRATCHPAD; + * buffer[1] = TA1; + * buffer[2] = TA2; + * buffer[3] = ES; + * + * // copy 0x0FF into the buffer, this effectively reads + * System.arraycopy(FF, 0, buffer, 4, 5); + * + * // read 5 extra bytes + * adapter.dataBlock(buffer, 0, 9); + * + * // if the last byte has not shown alternating output, + * // still call waitForSuccessfulFinish(), else + * // we are already done + * if (buffer[8] == (byte) 0x0ff) + * return waitForSuccessfulFinish(); + * else + * return true; + ** *
* The second method is faster because it is more expensive to invoke another * method that goes down to the native access layer than it is to just read a * few more bytes while the program is already at the native access layer. - *
* ** See the datasheet for which operations function in this manner. Only call * this method after another method which has successfully communicated with the * DS1963S. - *
* * @returntrue
if the DS1963S completed its operation successfully
*
@@ -1668,7 +1668,6 @@ private synchronized boolean write_read_copy_quick(int secret_page, int secret_o
* However, this method makes several optimizations to help it run faster.
* Because of the optimizations, this is the preferred way of writing data to a
* normal memory page on the DS1963S.
- *
*
* @param page_number page number to write
* @param page_data page data to write (must be at least 32 bytes long)
diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1A.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1A.java
index e199895a9d8..0ab175c3ca7 100644
--- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1A.java
+++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1A.java
@@ -56,8 +56,7 @@
* readRegister()
+ * @param register current register for conditional search, which if returned
+ * from readRegister()
*/
public void setConditionalSearchLogicLevel(byte[] register) {
if ((register[2] & (byte) 0x01) == (byte) 0x01) {
diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1D.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1D.java
index b1813d7f1f8..64d0e33ac3d 100644
--- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1D.java
+++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1D.java
@@ -69,8 +69,7 @@
* debouncing compatible with reed and Wiegand switches
* true
to see debug messages, false
to
- * suppress them
+ * @param onoff true
to see debug messages, false
to
+ * suppress them
*/
public static final void setDebugMode(boolean onoff) {
DEBUG = onoff;
@@ -100,7 +100,7 @@ public static final boolean getDebugMode() {
/**
* Sets the output stream for printing the debug info.
*
- * @param out the output stream for printing the debug info.
+ * @param outStream the output stream for printing the debug info.
*/
public static final void setPrintStream(PrintStream outStream) {
out = outStream;
@@ -109,12 +109,18 @@ public static final void setPrintStream(PrintStream outStream) {
/**
* Prints the specified java.lang.String
object if debug mode is
* enabled. This method calls PrintStream.println(String)
, and
- * pre-pends the String
">> " to the message, so that if a program
- * were to call (when debug mode was enabled):
- * com.dalsemi.onewire.debug.Debug.debug("Some notification...");
- *
the resulting output would look like:
- * >> Some notification...
- *
+ * pre-pends the String
">> " to the message, so that if a
+ * program were to call (when debug mode was enabled):
+ *
+ * + * com.dalsemi.onewire.debug.Debug.debug("Some notification..."); + *+ * + * the resulting output would look like: + * + *
+ * >> Some notification... + ** * @param x the message to print out if in debug mode */ @@ -126,13 +132,19 @@ public static final void debug(String x) { /** * Prints the specified array of bytes with a given label if debug mode is * enabled. This method calls
PrintStream.println(String)
, and
- * pre-pends the String
">> " to the message, so that if a program
- * were to call (when debug mode was enabled):
- * com.dalsemi.onewire.debug.Debug.debug("Some notification...", myBytes);
- *
the resulting output would look like:
+ * pre-pends the String
">> " to the message, so that if a
+ * program were to call (when debug mode was enabled):
+ *
+ *
+ * com.dalsemi.onewire.debug.Debug.debug("Some notification...", myBytes);
+ *
+ *
+ * the resulting output would look like:
+ *
+ *
* >> my label
* >> FF F1 F2 F3 F4 F5 F6 FF
- *
+ *
*
* @param lbl the message to print out above the array
* @param bytes the byte array to print out
@@ -145,13 +157,19 @@ public static final void debug(String lbl, byte[] bytes) {
/**
* Prints the specified array of bytes with a given label if debug mode is
* enabled. This method calls PrintStream.println(String)
, and
- * pre-pends the String
">> " to the message, so that if a program
- * were to call (when debug mode was enabled):
- * com.dalsemi.onewire.debug.Debug.debug("Some notification...", myBytes, 0, 8);
- *
the resulting output would look like:
- * >> my label
- * >> FF F1 F2 F3 F4 F5 F6 FF
- *
+ * pre-pends the String
">> " to the message, so that if a
+ * program were to call (when debug mode was enabled):
+ *
+ * + * com.dalsemi.onewire.debug.Debug.debug("Some notification...", myBytes, 0, 8); + *+ * + * the resulting output would look like: + * + *
+ * >> my label + * >> FF F1 F2 F3 F4 F5 F6 FF + ** * @param lbl the message to print out above the array * @param bytes the byte array to print out @@ -182,18 +200,16 @@ public static final void debug(String lbl, byte[] bytes, int offset, int length) /** * Prints the specified exception with a given label if debug mode is enabled. * This method calls
PrintStream.println(String)
, and pre-pends the
- * String
">> " to the message, so that if a program were to call
- * (when debug mode was enabled):
+ * String
">>" to the message, so that if a program were to
+ * call (when debug mode was enabled):
* com.dalsemi.onewire.debug.Debug.debug("Some notification...", exception);
*
the resulting output would look like:
- * >> my label
- * >> OneWireIOException: Device Not Present
+ * >> my label
+ * >> OneWireIOException: Device Not Present
*
*
- * @param lbl the message to print out above the array
- * @param bytes the byte array to print out
- * @param offset the offset to start printing from the array
- * @param length the number of bytes to print from the array
+ * @param lbl the message to print out above the array
+ * @param t
*/
public static final void debug(String lbl, Throwable t) {
if (DEBUG) {
diff --git a/io.openems.edge.common/src/io/openems/edge/common/channel/Channel.java b/io.openems.edge.common/src/io/openems/edge/common/channel/Channel.java
index 7a775ec3b04..9da11c47245 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/channel/Channel.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/channel/Channel.java
@@ -21,15 +21,16 @@
*
* - a Channel-ID which is unique among the OpenemsComponent. (see
* {@link io.openems.edge.common.channel.ChannelId})
- *
- a {@link Doc} as static meta information. (via {@link #channelDoc()})
+ *
- a {@link Doc} as static meta information. (via
+ * {@link Channel#channelDoc()})
*
- a system-wide unique {@link ChannelAddress} built from Component-ID and
- * Channel-ID. (via {@link #address()}
+ * Channel-ID. (via {@link Channel#address()}
*
- a {@link OpenemsType} which needs to map to the generic parameter
- * <T>. (via {@link #getType()})
- *
- an (active) {@link Value}. (via {@link #value()})
+ * <T>. (via {@link Channel#getType()})
+ *
- an (active) {@link Value}. (via {@link Channel#value()})
*
- callback methods to listen on value updates and changes. (see
- * {@link #onChange(Consumer)}, {@link #onUpdate(Consumer)} and
- * {@link #onSetNextValue(Consumer)})
+ * {@link Channel#onChange(Consumer)}, {@link Channel#onUpdate(Consumer)} and
+ * {@link Channel#onSetNextValue(Consumer)})
*
*
*
diff --git a/io.openems.edge.common/src/io/openems/edge/common/channel/ChannelId.java b/io.openems.edge.common/src/io/openems/edge/common/channel/ChannelId.java
index 129ab8efab6..d7a707bead4 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/channel/ChannelId.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/channel/ChannelId.java
@@ -38,12 +38,31 @@ public static String channelIdUpperToCamel(String name) {
}
}
+ /**
+ * Converts a Channel-ID in UPPER_CAMEL format to the UPPER_UNDERSCORE format.
+ *
+ *
+ * Examples: converts "ActivePower" to "ACTIVE_POWER".
+ *
+ * @param name Channel-ID in UPPER_CAMEL format.
+ * @return the a Channel-ID in UPPER_UNDERSCORE format
+ */
+ public static String channelIdCamelToUpper(String name) {
+ if (name.startsWith("_")) {
+ // special handling for reserved Channel-IDs starting with "_".
+ return "_" + CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, name.substring(1));
+ } else {
+ return CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, name);
+ }
+ }
+
/**
* Lists all Channel-IDs of the given Channel-ID Enum in a form that is suitable
* for a InfluxDB-Query in a Grafana Dashboard.
*
*
- * To create a query, call this function like `ChannelId.printChannelIdsForInfluxQuery(FeneconMiniEss.ServiceInfoChannelId.values());`
+ * To create a query, call this function like
+ * `ChannelId.printChannelIdsForInfluxQuery(FeneconMiniEss.ServiceInfoChannelId.values());`
*
* @param channelIds the {@link ChannelId}s, e.g. from ChannelId.values().
*/
diff --git a/io.openems.edge.common/src/io/openems/edge/common/channel/Doc.java b/io.openems.edge.common/src/io/openems/edge/common/channel/Doc.java
index afe266e37d4..2c2136d6660 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/channel/Doc.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/channel/Doc.java
@@ -16,13 +16,13 @@
* Possible meta information include:
*
* - access-mode (read-only/read-write/write-only) flag
- * {@link #accessMode(AccessMode)}. Defaults to Read-Only.
- *
- expected OpenemsType via {@link #getType()}
- *
- descriptive text via {@link #getText()}
- *
- is debug mode activated via {@link #isDebug()}
- *
- callback on initialization of a Channel via {@link #getOnInitCallback()}
+ * {@link Doc#accessMode(AccessMode)}. Defaults to Read-Only.
+ *
- expected OpenemsType via {@link Doc#getType()}
+ *
- descriptive text via {@link Doc#getText()}
+ *
- is debug mode activated via {@link Doc#isDebug()}
+ *
- callback on initialization of a Channel via
+ * {@link Doc#getOnInitCallback()}
*
- *
*/
public interface Doc {
diff --git a/io.openems.edge.common/src/io/openems/edge/common/channel/EnumDoc.java b/io.openems.edge.common/src/io/openems/edge/common/channel/EnumDoc.java
index b67f1a7c591..0527ca8c07b 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/channel/EnumDoc.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/channel/EnumDoc.java
@@ -85,7 +85,7 @@ public OptionsEnum getUndefinedOption() {
*
* @param name the name of the option. Comparison is case insensitive
* @return the {@link OptionsEnum}
- * @throws Throws OpenemsNamedException if there is no option with that name
+ * @throws OpenemsNamedException if there is no option with that name
*/
public OptionsEnum getOptionFromString(String name) throws OpenemsNamedException {
for (OptionsEnum e : this.options) {
@@ -101,7 +101,7 @@ public OptionsEnum getOptionFromString(String name) throws OpenemsNamedException
*
* @param name the name of the option. Comparison is case insensitive
* @return the integer value of the {@link OptionsEnum}
- * @throws Throws OpenemsNamedException if there is no option with that name
+ * @throws OpenemsNamedException if there is no option with that name
*/
public int getOptionValueFromString(String name) throws OpenemsNamedException {
return this.getOptionFromString(name).getValue();
diff --git a/io.openems.edge.common/src/io/openems/edge/common/channel/internal/AbstractDoc.java b/io.openems.edge.common/src/io/openems/edge/common/channel/internal/AbstractDoc.java
index 1a331dbded5..a171a31fb1a 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/channel/internal/AbstractDoc.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/channel/internal/AbstractDoc.java
@@ -141,10 +141,9 @@ public AbstractDoc onInit(Consumer> callback) {
}
/**
- * Gets the callbacks for initialization of the actual Channel
+ * Gets the callbacks for initialization of the actual Channel.
*
- * @param callback the method to call on initialization
- * @return myself
+ * @return a list of callbacks
*/
protected List>> getOnInitCallbacks() {
return onInitCallback;
diff --git a/io.openems.edge.common/src/io/openems/edge/common/component/ComponentManager.java b/io.openems.edge.common/src/io/openems/edge/common/component/ComponentManager.java
index ac4797f7175..08dc3de5cff 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/component/ComponentManager.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/component/ComponentManager.java
@@ -173,6 +173,15 @@ public default void _setDefaultConfigurationFailed(boolean value) {
*/
public List getEnabledComponents();
+ /**
+ * Gets all enabled OpenEMS-Components of the given Type.
+ *
+ * @param the given Type, subclass of {@link OpenemsComponent}
+ * @param clazz the given Type, subclass of {@link OpenemsComponent}
+ * @return a List of OpenEMS-Components
+ */
+ public List getEnabledComponentsOfType(Class clazz);
+
/**
* Gets all OpenEMS-Components.
*
diff --git a/io.openems.edge.common/src/io/openems/edge/common/component/OpenemsComponent.java b/io.openems.edge.common/src/io/openems/edge/common/component/OpenemsComponent.java
index e8f03e152a7..c431421dfa1 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/component/OpenemsComponent.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/component/OpenemsComponent.java
@@ -388,7 +388,11 @@ public static void updateConfigurationProperty(ConfigurationAdmin cm, String pid
*/
public static void logDebug(OpenemsComponent component, Logger log, String message) {
// TODO use log.debug(String, Object...) to improve speed
- log.debug("[" + component.id() + "] " + message);
+ if (component != null) {
+ log.debug("[" + component.id() + "] " + message);
+ } else {
+ log.debug(message);
+ }
}
/**
@@ -399,7 +403,11 @@ public static void logDebug(OpenemsComponent component, Logger log, String messa
* @param message the message
*/
public static void logInfo(OpenemsComponent component, Logger log, String message) {
- log.info("[" + component.id() + "] " + message);
+ if (component != null) {
+ log.info("[" + component.id() + "] " + message);
+ } else {
+ log.info(message);
+ }
}
/**
@@ -410,7 +418,11 @@ public static void logInfo(OpenemsComponent component, Logger log, String messag
* @param message the message
*/
public static void logWarn(OpenemsComponent component, Logger log, String message) {
- log.warn("[" + component.id() + "] " + message);
+ if (component != null) {
+ log.warn("[" + component.id() + "] " + message);
+ } else {
+ log.warn(message);
+ }
}
/**
@@ -421,7 +433,11 @@ public static void logWarn(OpenemsComponent component, Logger log, String messag
* @param message the message
*/
public static void logError(OpenemsComponent component, Logger log, String message) {
- log.error("[" + component.id() + "] " + message);
+ if (component != null) {
+ log.error("[" + component.id() + "] " + message);
+ } else {
+ log.error(message);
+ }
}
}
diff --git a/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/PolyLine.java b/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/PolyLine.java
index b7d40562ea6..8f6d0f4054b 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/PolyLine.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/PolyLine.java
@@ -8,6 +8,7 @@
import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.utils.JsonUtils;
+import io.openems.edge.common.type.TypeUtils;
/**
* Defines a polyline built of multiple points defined by a JsonArray.
@@ -17,7 +18,57 @@
*/
public class PolyLine {
- private final TreeMap points;
+ private final TreeMap points;
+
+ public static class Builder {
+ private final TreeMap points = new TreeMap<>();
+
+ private Builder() {
+ }
+
+ public Builder addPoint(double x, Double y) {
+ this.points.put(x, y);
+ return this;
+ }
+
+ public Builder addPoint(double x, double y) {
+ this.points.put(x, y);
+ return this;
+ }
+
+ public PolyLine build() {
+ return new PolyLine(this.points);
+ }
+ }
+
+ /**
+ * Create a PolyLine builder.
+ *
+ * @return a {@link Builder}
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ /**
+ * Create a PolyLine that returns null for every 'x'.
+ *
+ * @return a {@link PolyLine}
+ */
+ public static PolyLine empty() {
+ return new PolyLine((Double) null);
+ }
+
+ /**
+ * Creates a static PolyLine, i.e. the 'y' value is the same for each 'x'.
+ *
+ * @param y 'y' value
+ */
+ public PolyLine(Double y) {
+ TreeMap points = new TreeMap<>();
+ points.put(0D, y);
+ this.points = points;
+ }
/**
* Creates a PolyLine from two points.
@@ -26,15 +77,23 @@ public class PolyLine {
* @param y1 'y' value of point 1
* @param x2 'x' value of point 2
* @param y2 'y' value of point 2
- * @throws OpenemsNamedException on error
*/
- public PolyLine(Float x1, Float y1, Float x2, Float y2) throws OpenemsNamedException {
- TreeMap points = new TreeMap<>();
+ public PolyLine(double x1, Double y1, double x2, Double y2) {
+ TreeMap points = new TreeMap<>();
points.put(x1, y1);
points.put(x2, y2);
this.points = points;
}
+ /**
+ * Creates a PolyLine from a map of points.
+ *
+ * @param points a map of points
+ */
+ public PolyLine(TreeMap points) {
+ this.points = points;
+ }
+
/**
* Creates a PolyLine from a JSON line configuration.
*
@@ -69,10 +128,10 @@ public PolyLine(String x, String y, String lineConfig) throws OpenemsNamedExcept
* @throws OpenemsNamedException on error
*/
public PolyLine(String x, String y, JsonArray lineConfig) throws OpenemsNamedException {
- TreeMap points = new TreeMap<>();
+ TreeMap points = new TreeMap<>();
for (JsonElement element : lineConfig) {
- Float xValue = JsonUtils.getAsFloat(element, x);
- Float yValue = JsonUtils.getAsFloat(element, y);
+ Double xValue = JsonUtils.getAsDouble(element, x);
+ Double yValue = JsonUtils.getAsDouble(element, y);
points.put(xValue, yValue);
}
this.points = points;
@@ -81,13 +140,16 @@ public PolyLine(String x, String y, JsonArray lineConfig) throws OpenemsNamedExc
/**
* Gets the Y-value for the given X.
*
- * @param x the 'x' value
- * @return the 'y' value
- * @throws OpenemsNamedException on error
+ * @param x the 'x' value, possibly null
+ * @return the 'y' value, possibly null
*/
- public Float getValue(float x) throws OpenemsNamedException {
- Entry floorEntry = this.points.floorEntry(x);
- Entry ceilingEntry = this.points.ceilingEntry(x);
+ public Double getValue(Double x) {
+ if (x == null) {
+ return null;
+ }
+
+ Entry floorEntry = this.points.floorEntry(x);
+ Entry ceilingEntry = this.points.ceilingEntry(x);
if (floorEntry == null && ceilingEntry == null) {
return null;
@@ -102,9 +164,39 @@ public Float getValue(float x) throws OpenemsNamedException {
return floorEntry.getValue();
} else {
- Float m = (ceilingEntry.getValue() - floorEntry.getValue()) / (ceilingEntry.getKey() - floorEntry.getKey());
- Float t = floorEntry.getValue() - m * floorEntry.getKey();
+ Double m = (ceilingEntry.getValue() - floorEntry.getValue())
+ / (ceilingEntry.getKey() - floorEntry.getKey());
+ Double t = floorEntry.getValue() - m * floorEntry.getKey();
return m * x + t;
}
}
+
+ /**
+ * Gets the Y-value for the given X. Convenience method that internally converts
+ * the Float to a Double.
+ *
+ * @param x the 'x' value, possibly null
+ * @return the 'y' value, possibly null
+ */
+ public Double getValue(Float x) {
+ return this.getValue(TypeUtils.toDouble(x));
+ }
+
+ /**
+ * Gets the Y-value for the given X. Convenience method that internally converts
+ * the Integer to a Double.
+ *
+ * @param x the 'x' value, possibly null
+ * @return the 'y' value, possibly null
+ */
+ public Double getValue(Integer x) {
+ return this.getValue(TypeUtils.toDouble(x));
+ }
+
+ public static void printAsCsv(PolyLine polyLine) {
+ System.out.println("x;y");
+ for (Entry point : polyLine.points.entrySet()) {
+ System.out.println(point.getKey() + ";" + point.getValue());
+ }
+ }
}
diff --git a/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractContext.java b/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractContext.java
index 558d7a37b81..33554a9c8b4 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractContext.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractContext.java
@@ -8,6 +8,13 @@ public class AbstractContext {
private final PARENT parent;
+ /**
+ * Constructs an {@link AbstractContext} without useful logging.
+ */
+ public AbstractContext() {
+ this(null);
+ }
+
/**
* Constructs an {@link AbstractContext}.
*
@@ -24,7 +31,7 @@ public AbstractContext(PARENT parent) {
* @return the parent
*/
public PARENT getParent() {
- return parent;
+ return this.parent;
}
/**
diff --git a/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractStateMachine.java b/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractStateMachine.java
index 3a3938b3835..87a89a8f854 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractStateMachine.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractStateMachine.java
@@ -110,7 +110,7 @@ public void run(CONTEXT context) throws OpenemsNamedException {
// Call StateMachine events on transition
if (lastState != this.state) {
- this.log.info("Changing StateMachine from [" + lastState + "] to [" + this.state + "]");
+ context.logInfo(this.log, "Changing StateMachine from [" + lastState + "] to [" + this.state + "]");
// On-Exit of the last State
try {
diff --git a/io.openems.edge.common/src/io/openems/edge/common/statemachine/StateHandler.java b/io.openems.edge.common/src/io/openems/edge/common/statemachine/StateHandler.java
index f0626af7545..f3ece76d7db 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/statemachine/StateHandler.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/statemachine/StateHandler.java
@@ -13,7 +13,7 @@ public abstract class StateHandler, CONTEXT> {
/**
* Runs the main logic of StateMachine State and returns the next State.
*
- * @param context the {@link Context}.
+ * @param context the {@link CONTEXT}.
* @return the next State
*/
protected abstract STATE runAndGetNextState(CONTEXT context) throws OpenemsNamedException;
diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java b/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java
index 1f7e1f6b246..4b4eef0faab 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java
@@ -5,6 +5,7 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
+import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.Arrays;
@@ -25,6 +26,7 @@
import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.exceptions.OpenemsException;
+import io.openems.common.function.ThrowingRunnable;
import io.openems.common.types.ChannelAddress;
import io.openems.common.types.OpenemsType;
import io.openems.edge.common.channel.Channel;
@@ -32,6 +34,8 @@
import io.openems.edge.common.channel.EnumDoc;
import io.openems.edge.common.channel.WriteChannel;
import io.openems.edge.common.channel.value.Value;
+import io.openems.edge.common.component.ClockProvider;
+import io.openems.edge.common.component.ComponentManager;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.event.EdgeEventConstants;
import io.openems.edge.common.type.TypeUtils;
@@ -53,17 +57,27 @@ public ChannelValue(ChannelAddress address, Object value) {
this.value = value;
}
+ /**
+ * Gets the {@link ChannelAddress}.
+ *
+ * @return the {@link ChannelAddress}
+ */
public ChannelAddress getAddress() {
- return address;
+ return this.address;
}
+ /**
+ * Gets the value {@link Object}.
+ *
+ * @return the {@link Object}
+ */
public Object getValue() {
- return value;
+ return this.value;
}
@Override
public String toString() {
- return address.toString() + ":" + value;
+ return this.address.toString() + ":" + this.value;
}
}
@@ -95,6 +109,14 @@ public static class TestCase {
private final String description;
private final List inputs = new ArrayList<>();
private final List outputs = new ArrayList<>();
+ private final List> onBeforeProcessImageCallbacks = new ArrayList<>();
+ private final List> onAfterProcessImageCallbacks = new ArrayList<>();
+ private final List> onBeforeControllersCallbacks = new ArrayList<>();
+ private final List> onExecuteControllersCallbacks = new ArrayList<>();
+ private final List> onAfterControllersCallbacks = new ArrayList<>();
+ private final List> onBeforeWriteCallbacks = new ArrayList<>();
+ private final List> onExecuteWriteCallbacks = new ArrayList<>();
+ private final List> onAfterWriteCallbacks = new ArrayList<>();
private TimeLeap timeleap = null;
@@ -111,21 +133,143 @@ public TestCase(String description) {
this.description = "#" + (++instanceCounter) + (description.isEmpty() ? "" : ": " + description);
}
+ /**
+ * Adds an input value for a Channel.
+ *
+ * @param address the {@link ChannelAddress}
+ * @param value the value {@link Object}
+ * @return myself
+ */
public TestCase input(ChannelAddress address, Object value) {
this.inputs.add(new ChannelValue(address, value));
return this;
}
+ /**
+ * Adds an expected output value for a Channel.
+ *
+ * @param address the {@link ChannelAddress}
+ * @param value the value {@link Object}
+ * @return myself
+ */
public TestCase output(ChannelAddress address, Object value) {
this.outputs.add(new ChannelValue(address, value));
return this;
}
+ /**
+ * Adds a simulated timeleap, i.e. simulates that a given amount of time passed.
+ *
+ * @param clock the active {@link TimeLeapClock}, i.e. provided to the
+ * system-under-test by a {@link ClockProvider} like
+ * {@link ComponentManager}.
+ * @param amountToAdd the amount that should be simulated
+ * @param unit the {@link TemporalUnit} of the amount, e.g. using the
+ * {@link ChronoUnit} enum
+ * @return myself
+ */
public TestCase timeleap(TimeLeapClock clock, long amountToAdd, TemporalUnit unit) {
this.timeleap = new TimeLeap(clock, amountToAdd, unit);
return this;
}
+ /**
+ * Adds a Callback that is called on
+ * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_PROCESS_IMAGE} event.
+ *
+ * @param callback the callback
+ * @return myself
+ */
+ public TestCase onBeforeProcessImage(ThrowingRunnable callback) {
+ this.onBeforeProcessImageCallbacks.add(callback);
+ return this;
+ }
+
+ /**
+ * Adds a Callback that is called on
+ * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_PROCESS_IMAGE} event.
+ *
+ * @param callback the callback
+ * @return myself
+ */
+ public TestCase onAfterProcessImage(ThrowingRunnable callback) {
+ this.onAfterProcessImageCallbacks.add(callback);
+ return this;
+ }
+
+ /**
+ * Adds a Callback that is called on
+ * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_CONTROLLERS} event.
+ *
+ * @param callback the callback
+ * @return myself
+ */
+ public TestCase onBeforeControllersCallbacks(ThrowingRunnable callback) {
+ this.onBeforeControllersCallbacks.add(callback);
+ return this;
+ }
+
+ /**
+ * Adds a Callback that is called after
+ * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_CONTROLLERS} and before
+ * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_CONTROLLERS}. events.
+ *
+ * @param callback the callback
+ * @return myself
+ */
+ public TestCase onExecuteControllersCallbacks(ThrowingRunnable callback) {
+ this.onExecuteControllersCallbacks.add(callback);
+ return this;
+ }
+
+ /**
+ * Adds a Callback that is called on
+ * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_CONTROLLERS} event.
+ *
+ * @param callback the callback
+ * @return myself
+ */
+ public TestCase onAfterControllersCallbacks(ThrowingRunnable callback) {
+ this.onAfterControllersCallbacks.add(callback);
+ return this;
+ }
+
+ /**
+ * Adds a Callback that is called on
+ * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_WRITE} event.
+ *
+ * @param callback the callback
+ * @return myself
+ */
+ public TestCase onBeforeWriteCallbacks(ThrowingRunnable callback) {
+ this.onBeforeWriteCallbacks.add(callback);
+ return this;
+ }
+
+ /**
+ * Adds a Callback that is called on
+ * {@link EdgeEventConstants#TOPIC_CYCLE_EXECUTE_WRITE} event.
+ *
+ * @param callback the callback
+ * @return myself
+ */
+ public TestCase onExecuteWriteCallbacks(ThrowingRunnable callback) {
+ this.onExecuteWriteCallbacks.add(callback);
+ return this;
+ }
+
+ /**
+ * Adds a Callback that is called on
+ * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_WRITE} event.
+ *
+ * @param callback the callback
+ * @return myself
+ */
+ public TestCase onAfterWriteCallbacks(ThrowingRunnable callback) {
+ this.onAfterWriteCallbacks.add(callback);
+ return this;
+ }
+
/**
* Applies the time leap to the clock.
*/
@@ -164,7 +308,6 @@ protected void applyInputs(Map components)
* Validates the output values.
*
* @param components Referenced components
- * @param index
* @throws Exception on validation failure
*/
protected void validateOutputs(Map components) throws Exception {
@@ -301,13 +444,35 @@ public SELF addReference(String memberName, Object object) throws Exception {
return this.self();
}
+ private boolean addReference(Class> clazz, String memberName, Object object)
+ throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
+ try {
+ Field field = clazz.getDeclaredField(memberName);
+ field.setAccessible(true);
+ field.set(this.sut, object);
+ return true;
+ } catch (NoSuchFieldException e) {
+ // Ignore. Try method.
+ if (this.invokeSingleArgMethod(clazz, memberName, object)) {
+ return true;
+ }
+ }
+ // If we are here, no matching field or method was found. Search in parent
+ // classes.
+ Class> parent = clazz.getSuperclass();
+ if (parent == null) {
+ return false; // reached 'java.lang.Object'
+ }
+ return this.addReference(parent, memberName, object);
+ }
+
/**
* Adds an available {@link OpenemsComponent}.
*
*
* If the provided Component is a {@link DummyComponentManager}.
*
- * @param component
+ * @param component the {@link OpenemsComponent}s
* @return itself, to use as a builder
*/
public SELF addComponent(OpenemsComponent component) {
@@ -352,7 +517,7 @@ public SELF activate(AbstractComponentConfig config) throws Exception {
}
// Now SUT can be added to the list, as it does have an ID now
- this.addComponent(sut);
+ this.addComponent(this.sut);
return this.self();
}
@@ -411,28 +576,6 @@ private void callDeactivate() throws IllegalAccessException, IllegalArgumentExce
method.invoke(this.sut);
}
- private boolean addReference(Class> clazz, String memberName, Object object)
- throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
- try {
- Field field = clazz.getDeclaredField(memberName);
- field.setAccessible(true);
- field.set(this.sut, object);
- return true;
- } catch (NoSuchFieldException e) {
- // Ignore. Try method.
- if (this.invokeSingleArgMethod(clazz, memberName, object)) {
- return true;
- }
- }
- // If we are here, no matching field or method was found. Search in parent
- // classes.
- Class> parent = clazz.getSuperclass();
- if (parent == null) {
- return false; // reached 'java.lang.Object'
- }
- return addReference(parent, memberName, object);
- }
-
private boolean invokeSingleArgMethod(Class> clazz, String methodName, Object arg)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
Method[] methods = clazz.getDeclaredMethods();
@@ -468,28 +611,42 @@ private boolean invokeSingleArgMethod(Class> clazz, String methodName, Object
public SELF next(TestCase testCase) throws Exception {
testCase.applyTimeLeap();
this.onBeforeProcessImage();
+ executeCallbacks(testCase.onBeforeProcessImageCallbacks);
this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE);
for (Channel> channel : this.getSut().channels()) {
channel.nextProcessImage();
}
testCase.applyInputs(this.components);
this.onAfterProcessImage();
+ executeCallbacks(testCase.onAfterProcessImageCallbacks);
this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE);
this.onBeforeControllers();
+ executeCallbacks(testCase.onBeforeControllersCallbacks);
this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS);
this.onExecuteControllers();
+ executeCallbacks(testCase.onExecuteControllersCallbacks);
this.onAfterControllers();
+ executeCallbacks(testCase.onAfterControllersCallbacks);
this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_AFTER_CONTROLLERS);
this.onBeforeWrite();
+ executeCallbacks(testCase.onBeforeWriteCallbacks);
this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_BEFORE_WRITE);
this.onExecuteWrite();
+ executeCallbacks(testCase.onExecuteWriteCallbacks);
this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE);
this.onAfterWrite();
+ executeCallbacks(testCase.onAfterWriteCallbacks);
this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE);
testCase.validateOutputs(this.components);
return this.self();
}
+ private static void executeCallbacks(List> callbacks) throws Exception {
+ for (ThrowingRunnable callback : callbacks) {
+ callback.run();
+ }
+ }
+
/**
* If the 'system-under-test' is a {@link EventHandler} call the
* {@link EventHandler#handleEvent(Event)} method.
@@ -507,7 +664,7 @@ protected void handleEvent(String topic) throws Exception {
/**
* This method is executed before the
- * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_PROCESS_IMAGE event.
+ * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_PROCESS_IMAGE} event.
*
* @throws OpenemsNamedException on error
*/
@@ -516,7 +673,7 @@ protected void onBeforeProcessImage() throws OpenemsNamedException {
/**
* This method is executed before the
- * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_PROCESS_IMAGE event.
+ * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_PROCESS_IMAGE} event.
*
* @throws OpenemsNamedException on error
*/
@@ -525,7 +682,7 @@ protected void onAfterProcessImage() throws OpenemsNamedException {
/**
* This method is executed before the
- * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_CONTROLLERS event.
+ * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_CONTROLLERS} event.
*
* @throws OpenemsNamedException on error
*/
@@ -533,8 +690,9 @@ protected void onBeforeControllers() throws OpenemsNamedException {
}
/**
- * This method is executed after TOPIC_CYCLE_BEFORE_CONTROLLERS and before
- * TOPIC_CYCLE_AFTER_CONTROLLERS.
+ * This method is executed after
+ * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_CONTROLLERS} and before
+ * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_CONTROLLERS}.
*
* @throws OpenemsNamedException on error
*/
@@ -543,7 +701,7 @@ protected void onExecuteControllers() throws OpenemsNamedException {
/**
* This method is executed before the
- * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_CONTROLLERS event.
+ * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_CONTROLLERS} event.
*
* @throws OpenemsNamedException on error
*/
@@ -552,7 +710,7 @@ protected void onAfterControllers() throws OpenemsNamedException {
/**
* This method is executed before the
- * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_WRITE event.
+ * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_WRITE} event.
*
* @throws OpenemsNamedException on error
*/
@@ -561,7 +719,7 @@ protected void onBeforeWrite() throws OpenemsNamedException {
/**
* This method is executed before the
- * {@link EdgeEventConstants#TOPIC_CYCLE_EXECUTE_WRITE event.
+ * {@link EdgeEventConstants#TOPIC_CYCLE_EXECUTE_WRITE} event.
*
* @throws OpenemsNamedException on error
*/
@@ -570,11 +728,11 @@ protected void onExecuteWrite() throws OpenemsNamedException {
/**
* This method is executed before
- * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_WRITE.
+ * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_WRITE}.
*
* @throws OpenemsNamedException on error
*/
- protected void onAfterWrite() {
+ protected void onAfterWrite() throws OpenemsNamedException {
}
diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java b/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java
index d43a68ead40..026a720dfba 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java
@@ -45,6 +45,18 @@ public List getAllComponents() {
return Collections.unmodifiableList(this.components);
}
+ @Override
+ @SuppressWarnings("unchecked")
+ public List getEnabledComponentsOfType(Class clazz) {
+ List result = new ArrayList<>();
+ for (OpenemsComponent component : this.components) {
+ if (component.getClass().isInstance(clazz)) {
+ result.add((T) component);
+ }
+ }
+ return result;
+ }
+
/**
* Specific for this Dummy implementation.
*
diff --git a/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java b/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java
index 52b2fce7697..5f725ee0d8b 100644
--- a/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java
+++ b/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java
@@ -7,7 +7,6 @@
import com.google.gson.JsonNull;
import com.google.gson.JsonPrimitive;
-import io.openems.common.exceptions.OpenemsException;
import io.openems.common.types.OpenemsType;
import io.openems.common.types.OptionsEnum;
import io.openems.edge.common.channel.value.Value;
@@ -376,9 +375,9 @@ public static Long sum(Long... values) {
* Safely subtract Integers.
*
*
- * - if minuend is null -> result is null
- *
- if subtrahend is null -> result is minuend
- *
- if both are null -> result is null
+ *
- if minuend is null -> result is null
+ *
- if subtrahend is null -> result is minuend
+ *
- if both are null -> result is null
*
*
* @param minuend the minuend of the subtraction
@@ -399,9 +398,9 @@ public static Integer subtract(Integer minuend, Integer subtrahend) {
* Safely subtract Longs.
*
*
- * - if minuend is null -> result is null
- *
- if subtrahend is null -> result is minuend
- *
- if both are null -> result is null
+ *
- if minuend is null -> result is null
+ *
- if subtrahend is null -> result is minuend
+ *
- if both are null -> result is null
*
*
* @param minuend the minuend of the subtraction
@@ -436,15 +435,33 @@ public static Integer multiply(Integer... factors) {
return result;
}
+ /**
+ * Safely multiply Doubles.
+ *
+ * @param factors the factors of the multiplication
+ * @return the result, possibly null if all factors are null
+ */
+ public static Double multiply(Double... factors) {
+ Double result = null;
+ for (Double factor : factors) {
+ if (result == null) {
+ result = factor;
+ } else if (factor != null) {
+ result *= factor;
+ }
+ }
+ return result;
+ }
+
/**
* Safely divides Integers.
*
*
- * - if dividend is null -> result is null
+ *
- if dividend is null -> result is null
*
*
- * @param minuend the dividend of the division
- * @param subtrahend the divisor of the division
+ * @param dividend the dividend of the division
+ * @param divisor the divisor of the division
* @return the result, possibly null
*/
public static Integer divide(Integer dividend, int divisor) {
@@ -458,11 +475,11 @@ public static Integer divide(Integer dividend, int divisor) {
* Safely divides Longs.
*
*
- * - if dividend is null -> result is null
+ *
- if dividend is null -> result is null
*
*
- * @param minuend the dividend of the division
- * @param subtrahend the divisor of the division
+ * @param dividend the dividend of the division
+ * @param divisor the divisor of the division
* @return the result, possibly null
*/
public static Long divide(Long dividend, long divisor) {
@@ -491,6 +508,25 @@ public static Integer max(Integer... values) {
return result;
}
+ /**
+ * Safely finds the min value of all values.
+ *
+ * @return the min value; or null if all values are null
+ */
+ public static Double min(Double... values) {
+ Double result = null;
+ for (Double value : values) {
+ if (value != null) {
+ if (result == null) {
+ result = value;
+ } else {
+ result = Math.min(result, value);
+ }
+ }
+ }
+ return result;
+ }
+
/**
* Safely finds the average value of all values.
*
@@ -511,6 +547,28 @@ public static Float average(Integer... values) {
return sum / count;
}
+ /**
+ * Safely finds the average value of all values.
+ *
+ * @return the average value; or Double.NaN if all values are invalid.
+ */
+ public static double average(double... values) {
+ int count = 0;
+ double sum = 0.;
+ for (double value : values) {
+ if (Double.isNaN(value)) {
+ continue;
+ } else {
+ count++;
+ sum += value;
+ }
+ }
+ if (count == 0) {
+ return Double.NaN;
+ }
+ return sum / count;
+ }
+
/**
* Safely finds the average value of all values and rounds the result to an
* Integer using {@link Math#round(float)}.
@@ -527,15 +585,45 @@ public static Integer averageRounded(Integer... values) {
}
/**
- * Throws an descriptive exception if the object is null.
+ * Throws a descriptive exception if any of the objects is null.
*
* @param description text that is added to the exception
- * @param object the object
- * @throws OpenemsException if object is null
+ * @param objects the objects
+ * @throws IllegalArgumentException if any object is null
*/
- public static void assertNull(String description, Object object) throws OpenemsException {
- if (object == null) {
- throw new OpenemsException(description + " value is null!");
+ public static void assertNull(String description, Object... objects) throws IllegalArgumentException {
+ for (Object object : objects) {
+ if (object == null) {
+ throw new IllegalArgumentException(description + ": value is null!");
+ }
+ }
+ }
+
+ /**
+ * Safely convert from {@link Integer} to {@link Double}
+ *
+ * @param value the Integer value, possibly null
+ * @return the Double value, possibly null
+ */
+ public static Double toDouble(Integer value) {
+ if (value == null) {
+ return (Double) null;
+ } else {
+ return Double.valueOf(value);
+ }
+ }
+
+ /**
+ * Safely convert from {@link Float} to {@link Double}
+ *
+ * @param value the Float value, possibly null
+ * @return the Double value, possibly null
+ */
+ public static Double toDouble(Float value) {
+ if (value == null) {
+ return (Double) null;
+ } else {
+ return Double.valueOf(value);
}
}
diff --git a/io.openems.edge.common/test/io/openems/edge/common/linecharacteristic/PolyLineTest.java b/io.openems.edge.common/test/io/openems/edge/common/linecharacteristic/PolyLineTest.java
index 188806d300e..e805742f4c8 100644
--- a/io.openems.edge.common/test/io/openems/edge/common/linecharacteristic/PolyLineTest.java
+++ b/io.openems.edge.common/test/io/openems/edge/common/linecharacteristic/PolyLineTest.java
@@ -35,19 +35,19 @@ public void test() throws OpenemsNamedException {
PolyLine polyline = new PolyLine("xCoord", "yCoord", lineConfig);
// exactly first
- assertEquals(60f, polyline.getValue(0.9f), 0.00001);
+ assertEquals(60f, polyline.getValue(0.9), 0.00001);
// exactly last
- assertEquals(-60f, polyline.getValue(1.1f), 0.00001);
+ assertEquals(-60f, polyline.getValue(1.1), 0.00001);
// beyond last
- assertEquals(-60f, polyline.getValue(1.2f), 0.00001);
+ assertEquals(-60f, polyline.getValue(1.2), 0.00001);
// before first
- assertEquals(60f, polyline.getValue(0.7f), 0.00001);
+ assertEquals(60f, polyline.getValue(0.7), 0.00001);
// between first two
- assertEquals(30f, polyline.getValue(0.915f), 0.001);
+ assertEquals(30f, polyline.getValue(0.915), 0.001);
}
@Test
@@ -57,7 +57,7 @@ public void testEmpty() throws OpenemsNamedException {
PolyLine polyline = new PolyLine("xCoord", "yCoord", lineConfig);
// exactly first
- assertEquals(null, polyline.getValue(0.9f));
+ assertEquals(null, polyline.getValue(0.9));
}
}
diff --git a/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java b/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java
index c424e333a0e..e7b35ac3957 100644
--- a/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java
+++ b/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java
@@ -8,17 +8,14 @@ public class TypeUtilsTest {
@Test
public void testAverage() {
- // no values
- assertEquals(null, TypeUtils.average());
-
// null values
assertEquals(null, TypeUtils.average(null, null, null));
// int value
- assertEquals(2, Math.round(TypeUtils.average(1, 2, 3)));
+ assertEquals(Integer.valueOf(2), TypeUtils.averageRounded(1, 2, 3));
// float values
- assertEquals(2.5f, TypeUtils.average(2, 3), 0.001);
+ assertEquals(2.5f, TypeUtils.average(2F, 3F), 0.001);
// mixed values
assertEquals(2.5f, TypeUtils.average(2, null, 3), 0.001);
diff --git a/io.openems.edge.controller.api.modbus/readme.adoc b/io.openems.edge.controller.api.modbus/readme.adoc
index 95734ed7e0d..89d965f1dd4 100644
--- a/io.openems.edge.controller.api.modbus/readme.adoc
+++ b/io.openems.edge.controller.api.modbus/readme.adoc
@@ -1,5 +1,29 @@
= Api Modbus
-Provides a Modbus-Slave implementation for OpenEMS Edge. It provides access to Channels from an external device via Modbus/TCP.
+The OpenEMS Edge Modbus-Slave-API is provided by the "Modbus-API Controller".
+As the Modbus protocol is widely used in industrial monitoring and automation, this allows for easy access to OpenEMS channels from external systems.
+
+The configuration of the Modbus-API controller defines which OpenEMS Components should be exported and made available via the API.
+It then generates a dynamic Modbus protocol that is structured in address blocks that map to OpenEMS Components and Modbus register addresses that map to OpenEMS channels.
+
+The modbus table is designed in a way that allows dynamic parsing of all available registers.
+
+The following example describes a Controller that is configured to export the Sum-Component (`_sum`). By reading the headers of the individual blocks, the entire Modbus protocol can be parsed dynamically:
+
+. Register `0` identifies the system as an OpenEMS by the hash value `0x17ed6201`.
+
+. Register `1` shows the length of the first block. Adding the length (`199`) the current address (`1`) gives the starting address of the next block (`200`).
+
+. Register `200` gives a string with fixed length of 16 characters with the Component-ID.
+
+. Register `216` shows the length of the complete block. Adding the length (`300`) to the starting address of the block (`200`) gives the starting address of the next block (`500`) and so forth.
+
+. Register `220` identifies the first sub-block as Nature `OpenemsComponent`. The length of the sub-block follows in Register `221` and gives the starting address of the next sub-block (`300`) and so on.
+
+Instead of parsing the Modbus protocol, it is also possible to download the EMS specific Excel file via OpenEMS UI "System Profile" menu. An example export is available in the 'doc' folder of this bundle.
+
+To communicate with specific channels, it is then sufficient to read or write to the matching registers, e.g.
+- Read register `302 _sum/EssSoc` to get the total average state of charge of the ESS.
+- Write to register `806 ess0/SetActivePowerEquals` to trigger charging or discharging of the ESS with ID `ess0`.
https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.controller.api.modbus[Source Code icon:github[]]
\ No newline at end of file
diff --git a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristicImpl.java b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristicImpl.java
index ac4a9b0fdff..3f41c2351ea 100644
--- a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristicImpl.java
+++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristicImpl.java
@@ -125,7 +125,7 @@ public void run() throws OpenemsNamedException {
if (this.pByUCharacteristics == null) {
power = null;
} else {
- Float p = this.pByUCharacteristics.getValue(voltageRatio);
+ Double p = this.pByUCharacteristics.getValue(voltageRatio);
if (p == null) {
power = null;
} else {
diff --git a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java
index 8506905317a..ca31132129f 100644
--- a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java
+++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java
@@ -67,11 +67,11 @@ public void test() throws Exception {
).build()) //
.next(new TestCase("First Input") //
.input(METER_VOLTAGE, 250_000) // [mV]
- .output(ESS_ACTIVE_POWER, -2750)) //
+ .output(ESS_ACTIVE_POWER, -2749)) //
.next(new TestCase("Second Input, \"Power: -1500 \"") //
.timeleap(clock, 5, ChronoUnit.SECONDS) //
.input(METER_VOLTAGE, 248_000) // [mV]
- .output(ESS_ACTIVE_POWER, -1500))//
+ .output(ESS_ACTIVE_POWER, -1499))//
.next(new TestCase() //
.input(METER_VOLTAGE, 240_200) // [mV]
.output(ESS_ACTIVE_POWER, null)) //
@@ -88,7 +88,7 @@ public void test() throws Exception {
.next(new TestCase("Fourth Input, \"Power: 0 \"") //
.timeleap(clock, 5, ChronoUnit.SECONDS) //
.input(METER_VOLTAGE, 235_200) // [mV]
- .output(ESS_ACTIVE_POWER, 1000)) //
+ .output(ESS_ACTIVE_POWER, 998)) //
.next(new TestCase() //
.timeleap(clock, 2, ChronoUnit.SECONDS) //
.input(METER_VOLTAGE, 235_600) // [mV]
diff --git a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/AbstractPredictiveDelayCharge.java b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/AbstractPredictiveDelayCharge.java
index bcdfac2eb02..208e36abbb0 100644
--- a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/AbstractPredictiveDelayCharge.java
+++ b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/AbstractPredictiveDelayCharge.java
@@ -15,8 +15,8 @@
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.controller.api.Controller;
import io.openems.edge.ess.api.ManagedSymmetricEss;
-import io.openems.edge.predictor.api.ConsumptionHourlyPredictor;
-import io.openems.edge.predictor.api.ProductionHourlyPredictor;
+import io.openems.edge.predictor.api.hourly.ConsumptionHourlyPredictor;
+import io.openems.edge.predictor.api.hourly.ProductionHourlyPredictor;
public abstract class AbstractPredictiveDelayCharge extends AbstractOpenemsComponent implements OpenemsComponent {
diff --git a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/ac/AcPredictiveDelayCharge.java b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/ac/AcPredictiveDelayCharge.java
index 59bf5e0dd2a..d6cf1a77402 100644
--- a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/ac/AcPredictiveDelayCharge.java
+++ b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/ac/AcPredictiveDelayCharge.java
@@ -17,8 +17,8 @@
import io.openems.edge.ess.power.api.Phase;
import io.openems.edge.ess.power.api.Pwr;
import io.openems.edge.ess.power.api.Relationship;
-import io.openems.edge.predictor.api.ConsumptionHourlyPredictor;
-import io.openems.edge.predictor.api.ProductionHourlyPredictor;
+import io.openems.edge.predictor.api.hourly.ConsumptionHourlyPredictor;
+import io.openems.edge.predictor.api.hourly.ProductionHourlyPredictor;
@Designate(ocd = Config.class, factory = true)
@Component(//
diff --git a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/dc/DcPredictiveDelayCharge.java b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/dc/DcPredictiveDelayCharge.java
index efb8397a330..f2fef0611b0 100644
--- a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/dc/DcPredictiveDelayCharge.java
+++ b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/dc/DcPredictiveDelayCharge.java
@@ -18,8 +18,8 @@
import io.openems.edge.ess.power.api.Phase;
import io.openems.edge.ess.power.api.Pwr;
import io.openems.edge.ess.power.api.Relationship;
-import io.openems.edge.predictor.api.ConsumptionHourlyPredictor;
-import io.openems.edge.predictor.api.ProductionHourlyPredictor;
+import io.openems.edge.predictor.api.hourly.ConsumptionHourlyPredictor;
+import io.openems.edge.predictor.api.hourly.ProductionHourlyPredictor;
@Designate(ocd = Config.class, factory = true)
@Component(//
@@ -51,6 +51,7 @@ void activate(ComponentContext context, Config config) throws OpenemsNamedExcept
this.config = config;
}
+//
@Deactivate
protected void deactivate() {
super.deactivate();
diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePwrVoltChractersticImpl.java b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePwrVoltChractersticImpl.java
index 058a2a37b30..d5ac5a4721c 100644
--- a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePwrVoltChractersticImpl.java
+++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePwrVoltChractersticImpl.java
@@ -125,7 +125,7 @@ public void run() throws OpenemsNamedException {
if (this.qByUCharacteristics == null) {
percent = null;
} else {
- Float p = this.qByUCharacteristics.getValue(voltageRatio);
+ Double p = this.qByUCharacteristics.getValue(voltageRatio);
if (p == null) {
percent = null;
} else {
diff --git a/io.openems.edge.core/bnd.bnd b/io.openems.edge.core/bnd.bnd
index c4ff726c562..f492dc3d930 100644
--- a/io.openems.edge.core/bnd.bnd
+++ b/io.openems.edge.core/bnd.bnd
@@ -10,6 +10,7 @@ Bundle-Version: 1.0.0.${tstamp}
io.openems.edge.controller.api,\
io.openems.edge.ess.api,\
io.openems.edge.meter.api,\
+ io.openems.edge.predictor.api,\
io.openems.edge.scheduler.api,\
io.openems.edge.timedata.api,\
io.openems.wrapper.sdnotify
diff --git a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java
index d01d7d43f17..b6d91486094 100644
--- a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java
+++ b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java
@@ -132,6 +132,18 @@ public List getEnabledComponents() {
return Collections.unmodifiableList(this.enabledComponents);
}
+ @Override
+ @SuppressWarnings("unchecked")
+ public List getEnabledComponentsOfType(Class clazz) {
+ List result = new ArrayList<>();
+ for (OpenemsComponent component : this.enabledComponents) {
+ if (component.getClass().isInstance(clazz)) {
+ result.add((T) component);
+ }
+ }
+ return result;
+ }
+
@Override
public List getAllComponents() {
return Collections.unmodifiableList(this.allComponents);
diff --git a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/OutOfMemoryHeapDumpWorker.java b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/OutOfMemoryHeapDumpWorker.java
index e338e38dbf6..3e87bdd76c8 100644
--- a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/OutOfMemoryHeapDumpWorker.java
+++ b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/OutOfMemoryHeapDumpWorker.java
@@ -12,7 +12,7 @@
* This Worker constantly checks for heap-dump files in /usr/lib/openems
* directory. Those get created on OutOfMemory-Errors. All but the latest
* heap-dump file are deleted and the
- * {@link ComponentManagerImpl.ChannelId#WAS_OUT_OF_MEMORY} StateChannel is set.
+ * {@link ComponentManager.ChannelId#WAS_OUT_OF_MEMORY} StateChannel is set.
*/
public class OutOfMemoryHeapDumpWorker extends ComponentManagerWorker {
diff --git a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java
index 5609d8e8cd4..52569c3fe40 100644
--- a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java
+++ b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java
@@ -169,7 +169,7 @@ public NetworkConfiguration getNetworkConfiguration() throws OpenemsNamedExcepti
interfaces.put(name.get(), new NetworkInterface(name.get(), //
dhcp.get(), linkLocalAddressing.get(), gateway.get(), dns.get(), addresses.get(), file));
- } catch (IOException e) {
+ } catch (IllegalArgumentException | IOException e) {
throw new OpenemsException("Unable to read file [" + file + "]: " + e.getMessage());
}
}
diff --git a/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionRequest.java b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionRequest.java
new file mode 100644
index 00000000000..eb34b821b1e
--- /dev/null
+++ b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionRequest.java
@@ -0,0 +1,70 @@
+package io.openems.edge.core.predictormanager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+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.jsonrpc.base.JsonrpcRequest;
+import io.openems.common.types.ChannelAddress;
+import io.openems.common.utils.JsonUtils;
+
+/**
+ * Wraps a JSON-RPC Request to query a 24 Hours Prediction.
+ *
+ *
+ * {
+ * "jsonrpc": "2.0",
+ * "id": "UUID",
+ * "method": "get24HoursPrediction",
+ * "params": {
+ * "channels": string[]
+ * }
+ * }
+ *
+ */
+public class Get24HoursPredictionRequest extends JsonrpcRequest {
+
+ public static final String METHOD = "get24HoursPrediction";
+
+ public static Get24HoursPredictionRequest from(JsonrpcRequest r) throws OpenemsNamedException {
+ JsonObject p = r.getParams();
+ JsonArray cs = JsonUtils.getAsJsonArray(p, "channels");
+ List channels = new ArrayList<>();
+ for (JsonElement c : cs) {
+ channels.add(ChannelAddress.fromString(JsonUtils.getAsString(c)));
+ }
+ return new Get24HoursPredictionRequest(r.getId(), channels);
+ }
+
+ private final List channels;
+
+ public Get24HoursPredictionRequest(List channels) {
+ this(UUID.randomUUID(), channels);
+ }
+
+ public Get24HoursPredictionRequest(UUID id, List channels) {
+ super(id, METHOD);
+ this.channels = channels;
+ }
+
+ @Override
+ public JsonObject getParams() {
+ JsonArray channels = new JsonArray();
+ for (ChannelAddress channel : this.channels) {
+ channels.add(channel.toString());
+ }
+ return JsonUtils.buildJsonObject() //
+ .add("channels", channels) //
+ .build();
+ }
+
+ public List getChannels() {
+ return this.channels;
+ }
+
+}
diff --git a/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionResponse.java b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionResponse.java
new file mode 100644
index 00000000000..cf13edaaf9a
--- /dev/null
+++ b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionResponse.java
@@ -0,0 +1,57 @@
+package io.openems.edge.core.predictormanager;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.UUID;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess;
+import io.openems.common.types.ChannelAddress;
+import io.openems.edge.predictor.api.oneday.Prediction24Hours;
+
+/**
+ * Wraps a JSON-RPC Response to "get24HoursPrediction" Request.
+ *
+ *
+ *
+ *
+ * {
+ * "jsonrpc": "2.0",
+ * "id": "UUID",
+ * "result": {
+ * "componentId/channelId": [
+ * value1, value2,... // 96 values; one value per 15 minutes
+ * ]
+ * }
+ * }
+ *
+ */
+public class Get24HoursPredictionResponse extends JsonrpcResponseSuccess {
+
+ private final Map predictions;
+
+ public Get24HoursPredictionResponse(UUID id, Map predictions) {
+ super(id);
+ this.predictions = predictions;
+ }
+
+ @Override
+ public JsonObject getResult() {
+ JsonObject j = new JsonObject();
+ for (Entry entry : this.predictions.entrySet()) {
+ JsonArray values = new JsonArray();
+ for (Integer value : entry.getValue().getValues()) {
+ values.add(value);
+ }
+ j.add(entry.getKey().toString(), values);
+ }
+ return j;
+ }
+
+ public Map getPredictions() {
+ return predictions;
+ }
+
+}
diff --git a/io.openems.edge.core/src/io/openems/edge/core/predictormanager/PredictorManagerImpl.java b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/PredictorManagerImpl.java
new file mode 100644
index 00000000000..09ba90ca6a1
--- /dev/null
+++ b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/PredictorManagerImpl.java
@@ -0,0 +1,233 @@
+package io.openems.edge.core.predictormanager;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+
+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.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 io.openems.common.OpenemsConstants;
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.common.jsonrpc.base.JsonrpcRequest;
+import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess;
+import io.openems.common.session.User;
+import io.openems.common.types.ChannelAddress;
+import io.openems.edge.common.component.AbstractOpenemsComponent;
+import io.openems.edge.common.component.ComponentManager;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.common.jsonapi.JsonApi;
+import io.openems.edge.common.sum.Sum;
+import io.openems.edge.ess.dccharger.api.EssDcCharger;
+import io.openems.edge.meter.api.SymmetricMeter;
+import io.openems.edge.predictor.api.manager.PredictorManager;
+import io.openems.edge.predictor.api.oneday.Prediction24Hours;
+import io.openems.edge.predictor.api.oneday.Predictor24Hours;
+
+@Component(//
+ name = "Core.PredictorManager", //
+ immediate = true, //
+ property = { //
+ "id=" + OpenemsConstants.PREDICTOR_MANAGER_ID, //
+ "enabled=true" //
+ })
+public class PredictorManagerImpl extends AbstractOpenemsComponent
+ implements PredictorManager, OpenemsComponent, JsonApi {
+
+ @Reference
+ protected ComponentManager componentManager;
+
+ @Reference(policy = ReferencePolicy.DYNAMIC, //
+ policyOption = ReferencePolicyOption.GREEDY, //
+ cardinality = ReferenceCardinality.MULTIPLE, //
+ target = "(enabled=true)")
+ private volatile List predictors = new CopyOnWriteArrayList<>();
+
+ public PredictorManagerImpl() {
+ super(//
+ OpenemsComponent.ChannelId.values(), //
+ PredictorManager.ChannelId.values() //
+ );
+ }
+
+ @Activate
+ void activate(ComponentContext context) {
+ super.activate(context, OpenemsConstants.PREDICTOR_MANAGER_ID, "Core.PredictorManager", true);
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ super.deactivate();
+ }
+
+ @Override
+ public CompletableFuture extends JsonrpcResponseSuccess> handleJsonrpcRequest(User user, JsonrpcRequest request)
+ throws OpenemsNamedException {
+ switch (request.getMethod()) {
+ case Get24HoursPredictionRequest.METHOD:
+ return this.handleGet24HoursPredictionRequest(user, Get24HoursPredictionRequest.from(request));
+ }
+ return null;
+ }
+
+ private CompletableFuture extends JsonrpcResponseSuccess> handleGet24HoursPredictionRequest(User user,
+ Get24HoursPredictionRequest request) throws OpenemsNamedException {
+ final Map predictions = new HashMap<>();
+ for (ChannelAddress channel : request.getChannels()) {
+ predictions.put(channel, this.get24HoursPrediction(channel));
+ }
+ return CompletableFuture.completedFuture(new Get24HoursPredictionResponse(request.getId(), predictions));
+ }
+
+ @Override
+ public Prediction24Hours get24HoursPrediction(ChannelAddress channelAddress) {
+ Predictor24Hours predictor = this.getPredictorBestMatch(channelAddress);
+ if (predictor == null) {
+ // No explicit predictor found
+ if (channelAddress.getComponentId().equals(OpenemsConstants.SUM_ID)) {
+ // This is a Sum-Channel. Try to get predictions for each source channel.
+ Sum.ChannelId channelId = Sum.ChannelId.valueOf(
+ io.openems.edge.common.channel.ChannelId.channelIdCamelToUpper(channelAddress.getChannelId()));
+ return this.getPredictionSum(channelId);
+ } else {
+ return Prediction24Hours.EMPTY;
+ }
+ } else {
+ return predictor.get24HoursPrediction(channelAddress);
+ }
+ }
+
+ /**
+ * Gets the {@link Prediction24Hours} for a Sum-Channel.
+ *
+ * @param channelId the {@link Sum.ChannelId}
+ * @return the {@link Prediction24Hours}
+ */
+ private Prediction24Hours getPredictionSum(Sum.ChannelId channelId) {
+ switch (channelId) {
+ case CONSUMPTION_ACTIVE_ENERGY:
+ case CONSUMPTION_ACTIVE_POWER_L1:
+ case CONSUMPTION_ACTIVE_POWER_L2:
+ case CONSUMPTION_ACTIVE_POWER_L3:
+ case CONSUMPTION_MAX_ACTIVE_POWER:
+ case ESS_ACTIVE_CHARGE_ENERGY:
+ case ESS_ACTIVE_DISCHARGE_ENERGY:
+ case ESS_ACTIVE_POWER:
+ case ESS_ACTIVE_POWER_L1:
+ case ESS_ACTIVE_POWER_L2:
+ case ESS_ACTIVE_POWER_L3:
+ case ESS_CAPACITY:
+ case ESS_DC_CHARGE_ENERGY:
+ case ESS_DC_DISCHARGE_ENERGY:
+ case ESS_DISCHARGE_POWER:
+ case ESS_MAX_APPARENT_POWER:
+ case ESS_REACTIVE_POWER:
+ case ESS_SOC:
+ case GRID_ACTIVE_POWER:
+ case GRID_ACTIVE_POWER_L1:
+ case GRID_ACTIVE_POWER_L2:
+ case GRID_ACTIVE_POWER_L3:
+ case GRID_BUY_ACTIVE_ENERGY:
+ case GRID_MAX_ACTIVE_POWER:
+ case GRID_MIN_ACTIVE_POWER:
+ case GRID_MODE:
+ case GRID_SELL_ACTIVE_ENERGY:
+ case PRODUCTION_ACTIVE_ENERGY:
+ case PRODUCTION_AC_ACTIVE_ENERGY:
+ case PRODUCTION_AC_ACTIVE_POWER_L1:
+ case PRODUCTION_AC_ACTIVE_POWER_L2:
+ case PRODUCTION_AC_ACTIVE_POWER_L3:
+ case PRODUCTION_DC_ACTIVE_ENERGY:
+ case PRODUCTION_MAX_ACTIVE_POWER:
+ case PRODUCTION_MAX_AC_ACTIVE_POWER:
+ case PRODUCTION_MAX_DC_ACTUAL_POWER:
+ return Prediction24Hours.EMPTY;
+
+ case CONSUMPTION_ACTIVE_POWER:
+ // TODO
+ return Prediction24Hours.EMPTY;
+
+ case PRODUCTION_DC_ACTUAL_POWER: {
+ // Sum up "ActualPower" prediction of all EssDcChargers
+ List chargers = this.componentManager.getEnabledComponentsOfType(EssDcCharger.class);
+ Prediction24Hours[] predictions = new Prediction24Hours[chargers.size()];
+ for (int i = 0; i < chargers.size(); i++) {
+ EssDcCharger charger = chargers.get(i);
+ predictions[i] = this.get24HoursPrediction(
+ new ChannelAddress(charger.id(), EssDcCharger.ChannelId.ACTUAL_POWER.id()));
+ }
+ return Prediction24Hours.sum(predictions);
+ }
+ case PRODUCTION_AC_ACTIVE_POWER: {
+ // Sum up "ActivePower" prediction of all SymmetricMeters
+ List meters = this.componentManager.getEnabledComponentsOfType(SymmetricMeter.class)
+ .stream() //
+ .filter(meter -> {
+ switch (meter.getMeterType()) {
+ case GRID:
+ case CONSUMPTION_METERED:
+ case CONSUMPTION_NOT_METERED:
+ return false;
+ case PRODUCTION:
+ case PRODUCTION_AND_CONSUMPTION:
+ // Get only Production meters
+ return true;
+ }
+ // should never come here
+ return false;
+ }).collect(Collectors.toList());
+ Prediction24Hours[] predictions = new Prediction24Hours[meters.size()];
+ for (int i = 0; i < meters.size(); i++) {
+ SymmetricMeter meter = meters.get(i);
+ predictions[i] = this.get24HoursPrediction(
+ new ChannelAddress(meter.id(), SymmetricMeter.ChannelId.ACTIVE_POWER.id()));
+ }
+ return Prediction24Hours.sum(predictions);
+ }
+
+ case PRODUCTION_ACTIVE_POWER:
+ return Prediction24Hours.sum(//
+ this.getPredictionSum(Sum.ChannelId.PRODUCTION_AC_ACTIVE_POWER), //
+ this.getPredictionSum(Sum.ChannelId.PRODUCTION_DC_ACTUAL_POWER) //
+ );
+ }
+
+ // should never come here
+ return Prediction24Hours.EMPTY;
+ }
+
+ /**
+ * Gets the best matching {@link Predictor24Hours} for the given
+ * {@link ChannelAddress}.
+ *
+ * @param channelAddress the {@link ChannelAddress}
+ * @return the {@link Predictor24Hours} - or null if none matches
+ */
+ private synchronized Predictor24Hours getPredictorBestMatch(ChannelAddress channelAddress) {
+ int bestMatchValue = -1;
+ Predictor24Hours bestPredictor = null;
+ for (Predictor24Hours predictor : this.predictors) {
+ for (ChannelAddress pattern : predictor.getChannelAddresses()) {
+ int matchValue = ChannelAddress.match(channelAddress, pattern);
+ if (matchValue == 0) {
+ // Exact match
+ return predictor;
+ } else if (matchValue > bestMatchValue) {
+ bestMatchValue = matchValue;
+ bestPredictor = predictor;
+ }
+ }
+ }
+ return bestPredictor;
+ }
+
+}
diff --git a/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java b/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java
index caac3603044..8c599130a26 100644
--- a/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java
+++ b/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java
@@ -95,9 +95,9 @@ private void calculateChannelValues() {
final CalculateIntegerSum essActivePowerL1 = new CalculateIntegerSum();
final CalculateIntegerSum essActivePowerL2 = new CalculateIntegerSum();
final CalculateIntegerSum essActivePowerL3 = new CalculateIntegerSum();
-
+
final CalculateIntegerSum essReactivePower = new CalculateIntegerSum();
-
+
final CalculateIntegerSum essMaxApparentPower = new CalculateIntegerSum();
final CalculateGridMode essGridMode = new CalculateGridMode();
final CalculateLongSum essActiveChargeEnergy = new CalculateLongSum();
@@ -267,10 +267,10 @@ private void calculateChannelValues() {
this._setEssActivePowerL2(essActivePowerL2Sum);
Integer essActivePowerL3Sum = essActivePowerL3.calculate();
this._setEssActivePowerL3(essActivePowerL3Sum);
-
+
Integer essReactivePowerSum = essReactivePower.calculate();
this._setEssReactivePower(essReactivePowerSum);
-
+
Integer essMaxApparentPowerSum = essMaxApparentPower.calculate();
this._setEssMaxApparentPower(essMaxApparentPowerSum);
this._setGridMode(essGridMode.calculate());
diff --git a/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/data/InverterPrecision.java b/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/data/InverterPrecision.java
index 0774d1e0cb6..ce74d5528c0 100644
--- a/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/data/InverterPrecision.java
+++ b/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/data/InverterPrecision.java
@@ -22,19 +22,19 @@ public class InverterPrecision {
* Rounds each solution value to the Inverter precision; following this logic.
*
*
- * On Discharge (Power > 0)
+ * On Discharge (Power > 0)
*
*
- * - if SoC > 50 %: round up (more discharge)
- *
- if SoC < 50 %: round down (less discharge)
+ *
- if SoC > 50 %: round up (more discharge)
+ *
- if SoC < 50 %: round down (less discharge)
*
*
*
- * On Charge (Power < 0)
+ * On Charge (Power < 0)
*
*
- * - if SoC > 50 %: round down (less charge)
- *
- if SoC < 50 %: round up (more discharge)
+ *
- if SoC > 50 %: round down (less charge)
+ *
- if SoC < 50 %: round up (more discharge)
*
*
* @param coefficients the {@link Coefficients}
diff --git a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandler.java b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandler.java
new file mode 100644
index 00000000000..0b121b7b5ca
--- /dev/null
+++ b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandler.java
@@ -0,0 +1,155 @@
+package io.openems.edge.ess.generic.symmetric;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.function.BiConsumer;
+
+import io.openems.edge.battery.api.Battery;
+import io.openems.edge.common.channel.value.Value;
+import io.openems.edge.common.component.ClockProvider;
+import io.openems.edge.common.startstop.StartStoppable;
+import io.openems.edge.ess.api.ManagedSymmetricEss;
+
+/**
+ * Helper class to handle calculation of Allowed-Charge-Power and
+ * Allowed-Discharge-Power. This class is used by {@link ChannelManager} as a
+ * callback to updates of Battery Channels.
+ */
+public class AllowedChargeDischargeHandler implements BiConsumer {
+
+ public final static float DISCHARGE_EFFICIENCY_FACTOR = 0.95F;
+
+ /**
+ * Allow a maximum increase per second.
+ *
+ *
+ * 5 % of possible allowed charge/discharge power
+ */
+ public final static float MAX_INCREASE_PERCENTAGE = 0.05F;
+
+ private final ManagedSymmetricEss parent;
+
+ public AllowedChargeDischargeHandler(ManagedSymmetricEss parent) {
+ this.parent = parent;
+ }
+
+ protected float lastAllowedChargePower;
+ protected float lastAllowedDischargePower;
+ private Instant lastCalculate = null;
+
+ @Override
+ public void accept(ClockProvider clockProvider, Battery battery) {
+ Value chargeMaxCurrent = battery.getChargeMaxCurrentChannel().getNextValue();
+ Value dischargeMaxCurrent = battery.getDischargeMaxCurrentChannel().getNextValue();
+ Value voltage = battery.getVoltageChannel().getNextValue();
+
+ final boolean isStarted;
+ if (this.parent instanceof StartStoppable) {
+ isStarted = ((StartStoppable) this.parent).isStarted();
+ } else {
+ isStarted = true;
+ }
+
+ this.calculateAllowedChargeDischargePower(clockProvider, isStarted, //
+ chargeMaxCurrent.get(), dischargeMaxCurrent.get(), voltage.get());
+
+ // Apply AllowedChargePower and AllowedDischargePower
+ this.parent._setAllowedChargePower(Math.round(this.lastAllowedChargePower * -1 /* invert charge power */));
+ this.parent._setAllowedDischargePower(Math.round(this.lastAllowedDischargePower));
+ }
+
+ /**
+ * Calculates Allowed-Charge-Power and Allowed-Discharge Power from the given
+ * parameters. Result is stored in 'allowedChargePower' and
+ * 'allowedDischargePower' variables - both as positive values!
+ *
+ * @param isStarted is the ESS started?
+ * @param chargeMaxCurrent the {@link Battery.ChannelId#CHARGE_MAX_CURRENT}
+ * @param dischargeMaxCurrent the {@link Battery.ChannelId#DISHARGE_MAX_CURRENT}
+ * @param voltage the {@link Battery.ChannelId#VOLTAGE}
+ */
+ protected void calculateAllowedChargeDischargePower(ClockProvider clockProvider, boolean isStarted,
+ Integer chargeMaxCurrent, Integer dischargeMaxCurrent, Integer voltage) {
+ final Instant now = Instant.now(clockProvider.getClock());
+ float charge;
+ float discharge;
+
+ /*
+ * Calculate initial AllowedChargePower and AllowedDischargePower
+ */
+ if (!isStarted || chargeMaxCurrent == null || dischargeMaxCurrent == null || voltage == null) {
+ // Block ACTIVE and REACTIVE Power if
+ // - GenericEss is not in State "STARTED"
+ // - any of CHARGE_MAX_CURRENT, DISHARGE_MAX_CURRENT or VOLTAGE are missing
+ charge = 0;
+ discharge = 0;
+
+ } else {
+ // Calculate AllowedChargePower and AllowedDischargePower from battery current
+ // limits and voltage.
+ // Efficiency factor is not considered in chargeMaxCurrent (DC Power > AC Power)
+ charge = chargeMaxCurrent * voltage;
+ discharge = Math.round(dischargeMaxCurrent * voltage * DISCHARGE_EFFICIENCY_FACTOR);
+ }
+
+ /*
+ * Handle Force Charge and Discharge
+ */
+ if (charge < 0 && discharge < 0) {
+ // Both Force Charge and Discharge are active -> cannot do anything
+ charge = 0;
+ discharge = 0;
+
+ } else if (discharge < 0) {
+ // Force Charge is active
+ // Make sure AllowedChargePower is greater-or-equals absolute
+ // AllowedDischargePower
+ charge = Math.max(charge, Math.abs(discharge));
+
+ } else if (charge < 0) {
+ // Force Discharge is active
+ // Make sure AllowedDischargePower is greater-or-equals absolute
+ // AllowedChargePower
+ discharge = Math.max(Math.abs(charge), discharge);
+ }
+
+ /*
+ * In Non-Force Mode: apply the max increase ramp.
+ */
+ if (charge > 0) {
+ charge = applyMaxIncrease(this.lastAllowedChargePower, charge, this.lastCalculate, now);
+ }
+ if (discharge > 0) {
+ discharge = applyMaxIncrease(this.lastAllowedDischargePower, discharge, this.lastCalculate, now);
+ }
+
+ /*
+ * Apply result
+ */
+ this.lastCalculate = now;
+ this.lastAllowedChargePower = charge;
+ this.lastAllowedDischargePower = discharge;
+ }
+
+ /**
+ * Applies the max increase ramp, built from MAX_INCREASE_PERCENTAGE.
+ *
+ * @param lastValue the result value in [W] of previous run
+ * @param thisValue the current value [W]
+ * @param lastInstant the timestamp of the previous run
+ * @param thisInstant the current timestamp
+ * @return the new value
+ */
+ private static float applyMaxIncrease(float lastValue, float thisValue, Instant lastInstant, Instant thisInstant) {
+ final long millis;
+ if (lastValue < 0 || lastInstant == null) {
+ // was in Force-Mode before
+ lastValue = 0;
+ millis = 1000;
+ } else {
+ millis = Duration.between(lastInstant, thisInstant).toMillis();
+ }
+ return Math.min(thisValue, //
+ lastValue + (thisValue * millis * MAX_INCREASE_PERCENTAGE) / 1000.F /* convert [mW] to [W] */);
+ }
+}
\ No newline at end of file
diff --git a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/ChannelManager.java b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/ChannelManager.java
index 209ecc27853..fa47f02cefb 100644
--- a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/ChannelManager.java
+++ b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/ChannelManager.java
@@ -1,14 +1,12 @@
package io.openems.edge.ess.generic.symmetric;
-import java.util.function.Consumer;
-
import io.openems.edge.battery.api.Battery;
import io.openems.edge.batteryinverter.api.ManagedSymmetricBatteryInverter;
import io.openems.edge.batteryinverter.api.SymmetricBatteryInverter;
import io.openems.edge.common.channel.AbstractChannelListenerManager;
import io.openems.edge.common.channel.Channel;
import io.openems.edge.common.channel.ChannelId;
-import io.openems.edge.common.channel.value.Value;
+import io.openems.edge.common.component.ClockProvider;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.ess.api.SymmetricEss;
@@ -20,12 +18,11 @@
public class ChannelManager extends AbstractChannelListenerManager {
private final GenericManagedSymmetricEss parent;
-
- private float lastAllowedChargePower = 0;
- private float lastAllowedDischargePower = 0;
+ private final AllowedChargeDischargeHandler allowedChargeDischargeHandler;
public ChannelManager(GenericManagedSymmetricEss parent) {
this.parent = parent;
+ this.allowedChargeDischargeHandler = new AllowedChargeDischargeHandler(parent);
}
/**
@@ -34,83 +31,19 @@ public ChannelManager(GenericManagedSymmetricEss parent) {
* @param battery the {@link Battery}
* @param batteryInverter the {@link ManagedSymmetricBatteryInverter}
*/
- public void activate(Battery battery, ManagedSymmetricBatteryInverter batteryInverter) {
+ public void activate(ClockProvider clockProvider, Battery battery,
+ ManagedSymmetricBatteryInverter batteryInverter) {
/*
* Battery
*/
- final Consumer> allowedChargeDischargeCallback = (value) -> {
- // TODO: find proper efficiency factor to calculate AC Charge/Discharge limits
- // from DC
- final float efficiencyFactor = 0.95F;
-
- Value chargeMaxCurrent = battery.getChargeMaxCurrentChannel().getNextValue();
- Value dischargeMaxCurrent = battery.getDischargeMaxCurrentChannel().getNextValue();
- Value voltage = battery.getVoltageChannel().getNextValue();
-
- float allowedChargePower;
- float allowedDischargePower;
-
- /*
- * Calculate initial AllowedChargePower and AllowedDischargePower
- */
- if (!this.parent.isStarted()) {
- // If the GenericEss is not in State "STARTED" block ACTIVE and REACTIVE Power!
- allowedChargePower = 0;
- allowedDischargePower = 0;
-
- } else {
- // Calculate AllowedChargePower and AllowedDischargePower from battery current
- // limits and voltage
- // efficiency factor is not considered in chargeMaxCurrent (DC Power > AC Power)
- allowedChargePower = chargeMaxCurrent.get() * voltage.get() * -1;
- allowedDischargePower = dischargeMaxCurrent.get() * voltage.get() * efficiencyFactor;
- }
-
- /*
- * Allow max increase of 1 % per Call.
- *
- * NOTE: This code might be called multiple times per Cycle.
- */
- if (allowedChargePower < 0 && this.lastAllowedChargePower < 0) {
- allowedChargePower = Math.max(allowedChargePower, this.lastAllowedChargePower * 1.01F);
- }
- this.lastAllowedChargePower = allowedChargePower;
-
- if (allowedDischargePower > 0 && this.lastAllowedDischargePower > 0) {
- allowedDischargePower = Math.min(allowedDischargePower, this.lastAllowedDischargePower * 1.01F);
- }
- this.lastAllowedDischargePower = allowedDischargePower;
-
- /*
- * Handle Force Charge and Discharge
- */
- if (allowedChargePower > 0 && allowedDischargePower < 0) {
- // Both Force Charge and Discharge are active -> cannot do anything
- allowedChargePower = 0;
- allowedDischargePower = 0;
-
- } else if (allowedDischargePower < 0) {
- // Force Charge is active
- // Make sure AllowedChargePower is less-or-equals AllowedDischargePower
- allowedChargePower = Math.min(allowedChargePower, allowedDischargePower);
-
- } else if (allowedChargePower > 0) {
- // Force Discharge is active
- // Make sure AllowedDischargePower is greater-or-equals AllowedChargePower
- allowedDischargePower = Math.max(allowedChargePower, allowedDischargePower);
- }
-
- // Apply AllowedChargePower and AllowedDischargePower
- this.parent._setAllowedChargePower(Math.round(allowedChargePower));
- this.parent._setAllowedDischargePower(Math.round(allowedDischargePower));
- };
-
this.addOnSetNextValueListener(battery, Battery.ChannelId.DISCHARGE_MIN_VOLTAGE,
- allowedChargeDischargeCallback);
+ (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery));
this.addOnSetNextValueListener(battery, Battery.ChannelId.DISCHARGE_MAX_CURRENT,
- allowedChargeDischargeCallback);
- this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_VOLTAGE, allowedChargeDischargeCallback);
- this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_CURRENT, allowedChargeDischargeCallback);
+ (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery));
+ this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_VOLTAGE,
+ (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery));
+ this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_CURRENT,
+ (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery));
this.addCopyListener(battery, //
Battery.ChannelId.CAPACITY, //
SymmetricEss.ChannelId.CAPACITY);
diff --git a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssImpl.java b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssImpl.java
index ab87bc26b74..31d5fecd212 100644
--- a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssImpl.java
+++ b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssImpl.java
@@ -28,6 +28,7 @@
import io.openems.edge.batteryinverter.api.ManagedSymmetricBatteryInverter;
import io.openems.edge.batteryinverter.api.SymmetricBatteryInverter;
import io.openems.edge.common.component.AbstractOpenemsComponent;
+import io.openems.edge.common.component.ComponentManager;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.event.EdgeEventConstants;
import io.openems.edge.common.modbusslave.ModbusSlave;
@@ -60,6 +61,9 @@ public class GenericManagedSymmetricEssImpl extends AbstractOpenemsComponent imp
@Reference
private ConfigurationAdmin cm;
+ @Reference
+ private ComponentManager componentManager;
+
@Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY)
private ManagedSymmetricBatteryInverter batteryInverter;
@@ -105,7 +109,7 @@ void activate(ComponentContext context, Config config) {
return;
}
- this.channelHandler.activate(this.battery, this.batteryInverter);
+ this.channelHandler.activate(this.componentManager, this.battery, this.batteryInverter);
this.config = config;
}
@@ -211,7 +215,7 @@ public Constraint[] getStaticConstraints() throws OpenemsNamedException {
}
private AtomicReference startStopTarget = new AtomicReference(StartStop.UNDEFINED);
-
+
@Override
public void setStartStop(StartStop value) {
if (this.startStopTarget.getAndSet(value) != value) {
@@ -247,5 +251,5 @@ public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) {
SymmetricEss.getModbusSlaveNatureTable(accessMode), //
ManagedSymmetricEss.getModbusSlaveNatureTable(accessMode) //
);
- }
+ }
}
diff --git a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/package-info.java b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/package-info.java
new file mode 100644
index 00000000000..ab8586d01ed
--- /dev/null
+++ b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/package-info.java
@@ -0,0 +1,4 @@
+@org.osgi.annotation.versioning.Version("1.0.0")
+@org.osgi.annotation.bundle.Export
+// TODO remove, once Ess-Sinexcel is migrated to Battery-Inverter; see #1389
+package io.openems.edge.ess.generic.symmetric;
diff --git a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandlerTest.java b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandlerTest.java
new file mode 100644
index 00000000000..db3b760264d
--- /dev/null
+++ b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandlerTest.java
@@ -0,0 +1,90 @@
+package io.openems.edge.ess.generic.symmetric;
+
+import static org.junit.Assert.assertEquals;
+
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
+
+import org.junit.Test;
+
+import io.openems.edge.common.component.ClockProvider;
+import io.openems.edge.common.test.ComponentTest;
+import io.openems.edge.common.test.DummyComponentManager;
+import io.openems.edge.common.test.TimeLeapClock;
+
+public class AllowedChargeDischargeHandlerTest {
+
+ @Test
+ public void testStart() throws Exception {
+ final GenericManagedSymmetricEssImpl ess = new GenericManagedSymmetricEssImpl();
+ final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC);
+ final ClockProvider clockProvider = new DummyComponentManager(clock);
+ new ComponentTest(ess) //
+ .addReference("componentManager", clockProvider); //
+
+ AllowedChargeDischargeHandler sut = new AllowedChargeDischargeHandler(ess);
+
+ sut.calculateAllowedChargeDischargePower(clockProvider, false, null, null, null);
+ assertEquals(0, sut.lastAllowedChargePower, 0.001);
+ assertEquals(0, sut.lastAllowedDischargePower, 0.001);
+ clock.leap(1, ChronoUnit.SECONDS);
+
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, null, null, null);
+ assertEquals(0, sut.lastAllowedChargePower, 0.001);
+ assertEquals(0, sut.lastAllowedDischargePower, 0.001);
+ clock.leap(1, ChronoUnit.SECONDS);
+
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, -1, 500);
+ assertEquals(225, sut.lastAllowedChargePower, 0.001);
+ assertEquals(-475, sut.lastAllowedDischargePower, 0.001);
+
+ clock.leap(250, ChronoUnit.MILLIS);
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, -1, 500);
+ clock.leap(250, ChronoUnit.MILLIS);
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, -1, 500);
+ assertEquals(-475, sut.lastAllowedDischargePower, 0.001);
+ clock.leap(250, ChronoUnit.MILLIS);
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 0, 500);
+ clock.leap(250, ChronoUnit.MILLIS);
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 0, 500);
+ assertEquals(450, sut.lastAllowedChargePower, 0.001);
+ assertEquals(0, sut.lastAllowedDischargePower, 0.001);
+
+ clock.leap(1, ChronoUnit.SECONDS);
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 0, 500);
+ assertEquals(675, sut.lastAllowedChargePower, 0.001);
+ assertEquals(0, sut.lastAllowedDischargePower, 0.001);
+
+ for (int i = 0; i < 15; i++) {
+ clock.leap(1, ChronoUnit.SECONDS);
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 1, 500);
+ }
+
+ clock.leap(1, ChronoUnit.SECONDS);
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 1, 500);
+ assertEquals(4275, sut.lastAllowedChargePower, 0.001);
+ assertEquals(380, sut.lastAllowedDischargePower, 0.001);
+
+ clock.leap(1, ChronoUnit.SECONDS);
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 1, 500);
+ assertEquals(4500, sut.lastAllowedChargePower, 0.001);
+ assertEquals(403.75, sut.lastAllowedDischargePower, 0.001);
+
+ clock.leap(1, ChronoUnit.SECONDS);
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 2, 500);
+ assertEquals(4500, sut.lastAllowedChargePower, 0.001);
+ assertEquals(451.25, sut.lastAllowedDischargePower, 0.001);
+
+ clock.leap(1, ChronoUnit.SECONDS);
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 2, 0, 500);
+ assertEquals(1000, sut.lastAllowedChargePower, 0.001);
+ assertEquals(0, sut.lastAllowedDischargePower, 0.001);
+
+ clock.leap(1, ChronoUnit.SECONDS);
+ sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 9, 500);
+ assertEquals(1225, sut.lastAllowedChargePower, 0.001);
+ assertEquals(213.75, sut.lastAllowedDischargePower, 0.001);
+ }
+
+}
diff --git a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssTest.java b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssTest.java
index bf33eddc9fc..263a0e5a969 100644
--- a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssTest.java
+++ b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssTest.java
@@ -1,5 +1,8 @@
package io.openems.edge.ess.generic.symmetric;
+import java.time.Instant;
+import java.time.ZoneOffset;
+
import org.junit.Test;
import io.openems.common.types.ChannelAddress;
@@ -9,7 +12,9 @@
import io.openems.edge.common.startstop.StartStopConfig;
import io.openems.edge.common.test.AbstractComponentTest.TestCase;
import io.openems.edge.common.test.ComponentTest;
+import io.openems.edge.common.test.DummyComponentManager;
import io.openems.edge.common.test.DummyConfigurationAdmin;
+import io.openems.edge.common.test.TimeLeapClock;
import io.openems.edge.ess.generic.symmetric.statemachine.StateMachine.State;
import io.openems.edge.ess.test.DummyPower;
import io.openems.edge.ess.test.ManagedSymmetricEssTest;
@@ -36,8 +41,10 @@ public class GenericManagedSymmetricEssTest {
@Test
public void testStart() throws Exception {
+ final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC);
new ComponentTest(new GenericManagedSymmetricEssImpl()) //
.addReference("cm", new DummyConfigurationAdmin()) //
+ .addReference("componentManager", new DummyComponentManager(clock)) //
.addReference("batteryInverter", new DummyManagedSymmetricBatteryInverter(BATTERY_INVERTER_ID)) //
.addReference("battery", new DummyBattery(BATTERY_ID)) //
.activate(MyConfig.create() //
@@ -66,6 +73,7 @@ public void testForceCharge() throws Exception {
new ManagedSymmetricEssTest(new GenericManagedSymmetricEssImpl()) //
.addReference("power", new DummyPower()) //
.addReference("cm", new DummyConfigurationAdmin()) //
+ .addReference("componentManager", new DummyComponentManager()) //
.addReference("batteryInverter", new DummyManagedSymmetricBatteryInverter(BATTERY_INVERTER_ID)) //
.addReference("battery", new DummyBattery(BATTERY_ID) //
.withVoltage(500) //
diff --git a/io.openems.edge.ess.mr.gridcon/src/io/openems/edge/ess/mr/gridcon/Helper.java b/io.openems.edge.ess.mr.gridcon/src/io/openems/edge/ess/mr/gridcon/Helper.java
index 9cb3d31689c..67b6650bb63 100644
--- a/io.openems.edge.ess.mr.gridcon/src/io/openems/edge/ess/mr/gridcon/Helper.java
+++ b/io.openems.edge.ess.mr.gridcon/src/io/openems/edge/ess/mr/gridcon/Helper.java
@@ -9,7 +9,8 @@ public class Helper {
/**
* Checks if all API values of a battery are set.
- * @param Battery
+ *
+ * @param battery
* @return true if all API values are filled
*/
public static boolean isUndefined(Battery battery) {
diff --git a/io.openems.edge.ess.sinexcel/bnd.bnd b/io.openems.edge.ess.sinexcel/bnd.bnd
index 0107f4c2c2d..b9ee370190a 100644
--- a/io.openems.edge.ess.sinexcel/bnd.bnd
+++ b/io.openems.edge.ess.sinexcel/bnd.bnd
@@ -10,6 +10,7 @@ Bundle-Version: 1.0.0.${tstamp}
io.openems.edge.bridge.modbus,\
io.openems.edge.common,\
io.openems.edge.ess.api,\
+ io.openems.edge.ess.generic,\
io.openems.edge.meter.api
-testpath: \
diff --git a/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/ChannelManager.java b/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/ChannelManager.java
index a6d804eb9b2..3eca32dab8c 100644
--- a/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/ChannelManager.java
+++ b/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/ChannelManager.java
@@ -2,13 +2,17 @@
import io.openems.edge.battery.api.Battery;
import io.openems.edge.common.channel.AbstractChannelListenerManager;
+import io.openems.edge.common.component.ClockProvider;
+import io.openems.edge.ess.generic.symmetric.AllowedChargeDischargeHandler;
public class ChannelManager extends AbstractChannelListenerManager {
private final EssSinexcel parent;
+ private final AllowedChargeDischargeHandler allowedChargeDischargeHandler;
public ChannelManager(EssSinexcel parent) {
this.parent = parent;
+ this.allowedChargeDischargeHandler = new AllowedChargeDischargeHandler(parent);
}
/**
@@ -16,7 +20,16 @@ public ChannelManager(EssSinexcel parent) {
*
* @param battery the {@link Battery}
*/
- public void activate(Battery battery) {
+ public void activate(ClockProvider clockProvider, Battery battery) {
+ this.addOnSetNextValueListener(battery, Battery.ChannelId.DISCHARGE_MIN_VOLTAGE,
+ (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery));
+ this.addOnSetNextValueListener(battery, Battery.ChannelId.DISCHARGE_MAX_CURRENT,
+ (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery));
+ this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_VOLTAGE,
+ (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery));
+ this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_CURRENT,
+ (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery));
+
this.addOnChangeListener(battery, Battery.ChannelId.SOC, (oldValue, newValue) -> {
this.parent._setSoc(newValue.get());
this.parent.channel(EssSinexcel.ChannelId.BAT_SOC).setNextValue(newValue.get());
diff --git a/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/EssSinexcelImpl.java b/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/EssSinexcelImpl.java
index c9c7a9f7f2c..e1f67fe95df 100644
--- a/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/EssSinexcelImpl.java
+++ b/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/EssSinexcelImpl.java
@@ -27,6 +27,7 @@
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.ElementToChannelConverterChain;
import io.openems.edge.bridge.modbus.api.ModbusProtocol;
import io.openems.edge.bridge.modbus.api.element.BitsWordElement;
import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
@@ -79,8 +80,6 @@ public class EssSinexcelImpl extends AbstractOpenemsModbusComponent
protected int slowChargeVoltage = 4370; // for new batteries - 3940
protected int floatChargeVoltage = 4370; // for new batteries - 3940
- private int a = 0;
- private int counterOn = 0;
private int counterOff = 0;
// State-Machines
@@ -125,7 +124,7 @@ void activate(ComponentContext context, Config config) throws OpenemsNamedExcept
if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "Battery", config.battery_id())) {
return;
}
- this.channelHandler.activate(this.battery);
+ this.channelHandler.activate(this.componentManager, this.battery);
this.slowChargeVoltage = config.toppingCharge();
this.floatChargeVoltage = config.toppingCharge();
@@ -158,21 +157,16 @@ public EssSinexcelImpl() throws OpenemsNamedException {
private final static int MAX_CURRENT = 90; // [A]
- private float lastAllowedChargePower = 0;
- private float lastAllowedDischargePower = 0;
-
/**
* Sets the Battery Ranges. Executed on TOPIC_CYCLE_AFTER_PROCESS_IMAGE.
*
* @throws OpenemsNamedException
*/
private void setBatteryRanges() throws OpenemsNamedException {
- final float efficiencyFactor = 0.95F;
final int disMaxA;
final int chaMaxA;
final int disMinV;
final int chaMaxV;
- final int voltage;
// Evaluate input data
if (battery == null) {
@@ -180,13 +174,11 @@ private void setBatteryRanges() throws OpenemsNamedException {
chaMaxA = 0;
disMinV = 0;
chaMaxV = 0;
- voltage = 0;
} else {
disMaxA = battery.getDischargeMaxCurrent().orElse(0);
chaMaxA = battery.getChargeMaxCurrent().orElse(0);
disMinV = battery.getDischargeMinVoltage().orElse(0);
chaMaxV = battery.getChargeMaxVoltage().orElse(0);
- voltage = battery.getVoltage().orElse(0);
}
// Set Inverter Registers
@@ -212,36 +204,6 @@ private void setBatteryRanges() throws OpenemsNamedException {
IntegerWriteChannel chargeMaxVoltageChannel = this.channel(EssSinexcel.ChannelId.CHARGE_MAX_V);
chargeMaxVoltageChannel.setNextWriteValue(chaMaxV * 10);
}
-
- // Calculate AllowedCharge- and -DischargePower
- float allowedChargePower;
- float allowedDischargePower;
-
- // efficiency factor is not considered in chargeMaxCurrent (DC Power > AC Power)
- allowedChargePower = chaMaxA * voltage * -1;
- allowedDischargePower = disMaxA * voltage * efficiencyFactor;
-
- // Allow max increase of 1 %
- if (allowedDischargePower > lastAllowedDischargePower + allowedDischargePower * 0.01F) {
- allowedDischargePower = lastAllowedDischargePower + allowedDischargePower * 0.01F;
- }
- this.lastAllowedDischargePower = allowedDischargePower;
-
- if (allowedChargePower < lastAllowedChargePower + allowedChargePower * 0.01F) {
- allowedChargePower = lastAllowedChargePower + allowedChargePower * 0.01F;
- }
- this.lastAllowedChargePower = allowedChargePower;
-
- // Make sure solution is feasible
- if (allowedChargePower > allowedDischargePower) { // Force Discharge
- allowedDischargePower = allowedChargePower;
- }
- if (allowedDischargePower < allowedChargePower) { // Force Charge
- allowedChargePower = allowedDischargePower;
- }
-
- this._setAllowedChargePower(Math.round(allowedChargePower));
- this._setAllowedDischargePower(Math.round(allowedDischargePower));
}
/**
@@ -457,7 +419,8 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException {
new FC3ReadRegistersTask(0x0248, Priority.HIGH, //
m(SymmetricEss.ChannelId.ACTIVE_POWER, new SignedWordElement(0x0248), //
- ElementToChannelConverter.SCALE_FACTOR_1),
+ new ElementToChannelConverterChain(
+ ElementToChannelConverter.SCALE_FACTOR_1, IGNORE_LESS_THAN_100)),
new DummyRegisterElement(0x0249),
m(EssSinexcel.ChannelId.FREQUENCY, new SignedWordElement(0x024A),
ElementToChannelConverter.SCALE_FACTOR_MINUS_2),
@@ -626,27 +589,16 @@ public void applyPower(int activePower, int reactivePower) throws OpenemsNamedEx
IntegerWriteChannel setReactivePower = this.channel(EssSinexcel.ChannelId.SET_REACTIVE_POWER);
setReactivePower.setNextWriteValue(reactivePower / 100);
- if (this.stateOnOff() == false) {
- a = 1;
- }
-
- if (this.stateOnOff() == true) {
- a = 0;
- }
-
- if (activePower == 0 && reactivePower == 0 && a == 0) {
+ boolean isOn = this.stateOnOff();
+ if (activePower == 0 && reactivePower == 0 && isOn) {
this.counterOff++;
if (this.counterOff == 48) {
this.inverterOff();
this.counterOff = 0;
}
- } else if ((activePower != 0 || reactivePower != 0) && a == 1) {
- this.counterOn++;
- if (this.counterOn == 48) {
- this.inverterOn();
- this.counterOn = 0;
- }
+ } else if ((activePower != 0 || reactivePower != 0) && !isOn) {
+ this.inverterOn();
}
break;
@@ -706,4 +658,24 @@ public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) {
ManagedSymmetricEss.getModbusSlaveNatureTable(accessMode) //
);
}
+
+ /**
+ * The Sinexcel Battery Inverter claims to outputting a little bit of power even
+ * if it does not. This little filter ignores values for ActivePower less than
+ * 100 (charge/discharge).
+ */
+ private static final ElementToChannelConverter IGNORE_LESS_THAN_100 = new ElementToChannelConverter(//
+ obj -> {
+ if (obj == null) {
+ return null;
+ }
+ int value = (Short) obj;
+ if (Math.abs(value) < 100) {
+ return 0;
+ } else {
+ return value;
+ }
+ }, //
+ value -> value);
+
}
diff --git a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigSelfConsumption.java b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigSelfConsumption.java
index f886150cf11..568153c2c58 100644
--- a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigSelfConsumption.java
+++ b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigSelfConsumption.java
@@ -28,9 +28,6 @@
@AttributeDefinition(name = "Evcs target filter", description = "This is auto-generated by 'Evcs-IDs'.")
String Evcs_target() default "";
- @AttributeDefinition(name = "Ess-ID", description = "ID of Ess device.")
- String ess_id() default "ess0";
-
String webconsole_configurationFactory_nameHint() default "EVCS Cluster Self Consumption [{id}]";
}
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 13aff2efcce..d3d4678aaaf 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
@@ -105,7 +105,7 @@ void activate(ComponentContext context, ConfigPeakShaving config) throws Openems
this.config = config;
- // update filter for 'evcss' component
+ // update filter for 'evcs' component
if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "Evcs", config.evcs_ids())) {
return;
}
diff --git a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java
index d293c997386..bdf92e8e478 100644
--- a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java
+++ b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java
@@ -23,6 +23,7 @@
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
+import org.osgi.service.event.EventHandler;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,7 +38,7 @@
EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_AFTER_CONTROLLERS, //
EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE //
})
-public class EvcsClusterSelfConsumption extends AbstractEvcsCluster implements OpenemsComponent, Evcs {
+public class EvcsClusterSelfConsumption extends AbstractEvcsCluster implements OpenemsComponent, Evcs, EventHandler {
private final Logger log = LoggerFactory.getLogger(EvcsClusterSelfConsumption.class);
diff --git a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppServer.java b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppServer.java
index c46dadde8a9..82c217d5dd9 100644
--- a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppServer.java
+++ b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppServer.java
@@ -18,7 +18,7 @@ public interface OcppServer {
* Example:
*
*
- * send(session, request).whenComplete((confirmation, throwable) -> {
+ * send(session, request).whenComplete((confirmation, throwable) -> {
* this.logInfo(log, confirmation.toString());
* });
*
diff --git a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppStandardRequests.java b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppStandardRequests.java
index 7d5a4aac521..4e44cf41d30 100644
--- a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppStandardRequests.java
+++ b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppStandardRequests.java
@@ -12,7 +12,7 @@ public interface OcppStandardRequests {
* Attention: The given power is given in watt. EVCS with the charging type AC
* mostly send their limit values as amps.
*
- * @param ChargePower power that should be charged in watt.
+ * @param chargePower power that should be charged in watt.
* @return Valid request that can be sent to the EVCS.
*/
Request setChargePowerLimit(int chargePower);
diff --git a/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/MyJsonServer.java b/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/MyJsonServer.java
index d223b56d9ce..5ae2c7d611c 100644
--- a/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/MyJsonServer.java
+++ b/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/MyJsonServer.java
@@ -170,7 +170,7 @@ public void sendDefault(UUID session, Request request) {
* Sending initially all required requests to the EVCS.
*
* @param sessionIndex given session
- * @param evcs given evcs
+ * @param ocppEvcs given evcs
*/
protected void sendInitialRequests(UUID sessionIndex, AbstractOcppEvcsComponent ocppEvcs) {
// Setting the Evcss of this session id to available
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ConsumptionHourlyPredictor.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ConsumptionHourlyPredictor.java
similarity index 52%
rename from io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ConsumptionHourlyPredictor.java
rename to io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ConsumptionHourlyPredictor.java
index b65ef2e8068..7b5353f89fa 100644
--- a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ConsumptionHourlyPredictor.java
+++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ConsumptionHourlyPredictor.java
@@ -1,8 +1,9 @@
-package io.openems.edge.predictor.api;
+package io.openems.edge.predictor.api.hourly;
/**
* Provides a consumption prediction for the next 24 h.
*/
+// TODO remove the ConsumptionHourlyPredictor in favor of PredictorManager API
public interface ConsumptionHourlyPredictor extends HourlyPredictor {
}
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPrediction.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPrediction.java
similarity index 82%
rename from io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPrediction.java
rename to io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPrediction.java
index 13d8cedc74a..099efcc2a1d 100644
--- a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPrediction.java
+++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPrediction.java
@@ -1,10 +1,11 @@
-package io.openems.edge.predictor.api;
+package io.openems.edge.predictor.api.hourly;
import java.time.ZonedDateTime;
/**
* Holds a prediction for 24 h; one value per hour; starting from 'start' time.
*/
+//TODO remove the HourlyPrediction in favor of PredictorManager API
public class HourlyPrediction {
private final Integer[] values = new Integer[24];
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPredictor.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPredictor.java
similarity index 81%
rename from io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPredictor.java
rename to io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPredictor.java
index d2f5cff2d33..71087c9330a 100644
--- a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPredictor.java
+++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPredictor.java
@@ -1,4 +1,4 @@
-package io.openems.edge.predictor.api;
+package io.openems.edge.predictor.api.hourly;
import org.osgi.annotation.versioning.ProviderType;
@@ -8,6 +8,7 @@
* Provides a prediction for the next 24 h.
*/
@ProviderType
+//TODO remove the HourlyPredictor in favor of PredictorManager API
public interface HourlyPredictor extends OpenemsComponent {
/**
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ProductionHourlyPredictor.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ProductionHourlyPredictor.java
similarity index 59%
rename from io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ProductionHourlyPredictor.java
rename to io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ProductionHourlyPredictor.java
index 6a28eb17faa..e19ae0942b6 100644
--- a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ProductionHourlyPredictor.java
+++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ProductionHourlyPredictor.java
@@ -1,9 +1,10 @@
-package io.openems.edge.predictor.api;
+package io.openems.edge.predictor.api.hourly;
/**
* Provides a production prediction for the next 24 h; e.g. for a photovoltaics
* installation.
*/
+//TODO remove the ProductionHourlyPredictor in favor of PredictorManager API
public interface ProductionHourlyPredictor extends HourlyPredictor {
}
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/package-info.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/package-info.java
new file mode 100644
index 00000000000..5bec3f92937
--- /dev/null
+++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/package-info.java
@@ -0,0 +1,4 @@
+@org.osgi.annotation.versioning.Version("1.0.0")
+@org.osgi.annotation.bundle.Export
+//TODO remove in favor of PredictorManager API
+package io.openems.edge.predictor.api.hourly;
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/PredictorManager.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/PredictorManager.java
new file mode 100644
index 00000000000..ea8a49ebcda
--- /dev/null
+++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/PredictorManager.java
@@ -0,0 +1,33 @@
+package io.openems.edge.predictor.api.manager;
+
+import io.openems.common.types.ChannelAddress;
+import io.openems.edge.common.channel.Doc;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.predictor.api.oneday.Prediction24Hours;
+import io.openems.edge.predictor.api.oneday.Predictor24Hours;
+
+public interface PredictorManager extends 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;
+ }
+ }
+
+ /**
+ * Gets the {@link Prediction24Hours} by the best matching
+ * {@link Predictor24Hours} for the given {@link ChannelAddress}.
+ *
+ * @param channelAddress the {@link ChannelAddress}
+ * @return the {@link Prediction24Hours} - all values null if no Predictor
+ * matches the Channel-Address
+ */
+ public Prediction24Hours get24HoursPrediction(ChannelAddress channelAddress);
+}
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/package-info.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/package-info.java
new file mode 100644
index 00000000000..6dd9b2ce408
--- /dev/null
+++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/package-info.java
@@ -0,0 +1,3 @@
+@org.osgi.annotation.versioning.Version("1.0.0")
+@org.osgi.annotation.bundle.Export
+package io.openems.edge.predictor.api.manager;
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/AbstractPredictor24Hours.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/AbstractPredictor24Hours.java
new file mode 100644
index 00000000000..1a4ddd61c15
--- /dev/null
+++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/AbstractPredictor24Hours.java
@@ -0,0 +1,86 @@
+package io.openems.edge.predictor.api.oneday;
+
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoField;
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.osgi.service.component.ComponentContext;
+
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.common.types.ChannelAddress;
+import io.openems.edge.common.component.AbstractOpenemsComponent;
+import io.openems.edge.common.component.ClockProvider;
+import io.openems.edge.common.component.OpenemsComponent;
+
+public abstract class AbstractPredictor24Hours extends AbstractOpenemsComponent
+ implements Predictor24Hours, OpenemsComponent {
+
+ protected static class PredictionContainer {
+ private Prediction24Hours latestPrediction = null;
+ private ZonedDateTime latestPredictionTimestamp = null;
+ }
+
+ private final Map predictions = new HashMap<>();
+ private ChannelAddress[] channelAddresses = new ChannelAddress[0];
+
+ protected abstract ClockProvider getClockProvider();
+
+ protected abstract Prediction24Hours createNewPrediction(ChannelAddress channelAddress);
+
+ protected AbstractPredictor24Hours(io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds,
+ io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) {
+ super(firstInitialChannelIds, furtherInitialChannelIds);
+ }
+
+ protected final void activate(ComponentContext context, String id, String alias, boolean enabled) {
+ throw new IllegalArgumentException("use the other activate method!");
+ }
+
+ protected void activate(ComponentContext context, String id, String alias, boolean enabled,
+ String[] channelAddresses) throws OpenemsNamedException {
+ super.activate(context, id, alias, enabled);
+ ChannelAddress[] channelAddressesArray = new ChannelAddress[channelAddresses.length];
+ for (int i = 0; i < channelAddresses.length; i++) {
+ channelAddressesArray[i] = ChannelAddress.fromString(channelAddresses[i]);
+ }
+ this.channelAddresses = channelAddressesArray;
+ }
+
+ @Override
+ public ChannelAddress[] getChannelAddresses() {
+ return this.channelAddresses;
+ }
+
+ @Override
+ public Prediction24Hours get24HoursPrediction(ChannelAddress channelAddress) {
+ ZonedDateTime now = roundZonedDateTimeDownTo15Minutes(ZonedDateTime.now(this.getClockProvider().getClock()));
+ PredictionContainer container = this.predictions.get(channelAddress);
+ if (container == null) {
+ container = new PredictionContainer();
+ this.predictions.put(channelAddress, container);
+ }
+ if (container.latestPredictionTimestamp == null || now.isAfter(container.latestPredictionTimestamp)) {
+ // Create new prediction
+ Prediction24Hours prediction = this.createNewPrediction(channelAddress);
+ container.latestPrediction = prediction;
+ container.latestPredictionTimestamp = now;
+ } else {
+ // Reuse existing prediction
+ }
+ return container.latestPrediction;
+ }
+
+ /**
+ * Rounds a {@link ZonedDateTime} down to 15 minutes.
+ *
+ * @param d the {@link ZonedDateTime}
+ * @return the rounded result
+ */
+ private static ZonedDateTime roundZonedDateTimeDownTo15Minutes(ZonedDateTime d) {
+ int minuteOfDay = d.get(ChronoField.MINUTE_OF_DAY);
+ return d.with(ChronoField.NANO_OF_DAY, 0).plus(minuteOfDay / 15 * 15, ChronoUnit.MINUTES);
+ }
+
+}
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Prediction24Hours.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Prediction24Hours.java
new file mode 100644
index 00000000000..b435d771232
--- /dev/null
+++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Prediction24Hours.java
@@ -0,0 +1,64 @@
+package io.openems.edge.predictor.api.oneday;
+
+import io.openems.edge.common.type.TypeUtils;
+
+/**
+ * Holds a prediction for 24 h; one value per 15 minutes; 96 values in total.
+ *
+ *
+ * Values have the same unit as the base Channel, i.e. if the Prediction relates
+ * to _sum/ProductionGridActivePower, the value is in unit Watt and represents
+ * the average Watt within a 15 minutes period.
+ */
+public class Prediction24Hours {
+
+ public final static int NUMBER_OF_VALUES = 96;
+
+ private final Integer[] values = new Integer[NUMBER_OF_VALUES];
+
+ /**
+ * Holds a {@link Prediction24Hours} with all values null.
+ */
+ public final static Prediction24Hours EMPTY = new Prediction24Hours(new Integer[0]);
+
+ /**
+ * Sums up the given {@link Prediction24Hours}s. If any source value is null,
+ * the result value is also null.
+ *
+ * @param predictions the given {@link Prediction24Hours}
+ * @return a {@link Prediction24Hours} holding the sum of all predictions.
+ */
+ public static Prediction24Hours sum(Prediction24Hours... predictions) {
+ final Integer[] sumValues = new Integer[NUMBER_OF_VALUES];
+ for (int i = 0; i < NUMBER_OF_VALUES; i++) {
+ Integer sumValue = null;
+ for (Prediction24Hours prediction : predictions) {
+ if (prediction.values[i] == null) {
+ sumValue = null;
+ break;
+ } else {
+ sumValue = TypeUtils.sum(sumValue, prediction.values[i]);
+ }
+ }
+ sumValues[i] = sumValue;
+ }
+ return new Prediction24Hours(sumValues);
+ }
+
+ /**
+ * Constructs a {@link Prediction24Hours}.
+ *
+ * @param values the 96 prediction values
+ */
+ public Prediction24Hours(Integer[] values) {
+ super();
+ for (int i = 0; i < NUMBER_OF_VALUES && i < values.length; i++) {
+ this.values[i] = values[i];
+ }
+ }
+
+ public Integer[] getValues() {
+ return this.values;
+ }
+
+}
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Predictor24Hours.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Predictor24Hours.java
new file mode 100644
index 00000000000..3478e4559bb
--- /dev/null
+++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Predictor24Hours.java
@@ -0,0 +1,39 @@
+package io.openems.edge.predictor.api.oneday;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+import io.openems.common.types.ChannelAddress;
+import io.openems.edge.common.component.OpenemsComponent;
+
+/**
+ * Provides a prediction for the next 24 h; one value per 15 minutes; 96 values
+ * in total.
+ */
+@ProviderType
+public interface Predictor24Hours extends OpenemsComponent {
+
+ /**
+ * Gets the Channel-Addresses for which this Predictor can provide a prediction.
+ *
+ *
+ * The entries can contain wildcards to match multiple actual
+ * {@link ChannelAddress}es.
+ *
+ * @return an array of {@link ChannelAddress}es
+ */
+ public ChannelAddress[] getChannelAddresses();
+
+ /**
+ * Gives a prediction for the next 24 h for the given {@link ChannelAddress};
+ * 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.
+ *
+ * @param channelAddress the {@link ChannelAddress}
+ * @return the {@link Prediction24Hours}
+ */
+ public Prediction24Hours get24HoursPrediction(ChannelAddress channelAddress);
+
+}
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/package-info.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/package-info.java
new file mode 100644
index 00000000000..2b584eb847b
--- /dev/null
+++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/package-info.java
@@ -0,0 +1,3 @@
+@org.osgi.annotation.versioning.Version("1.0.0")
+@org.osgi.annotation.bundle.Export
+package io.openems.edge.predictor.api.oneday;
diff --git a/io.openems.edge.predictor.persistencemodel/bnd.bnd b/io.openems.edge.predictor.persistencemodel/bnd.bnd
index cbc64e94ea5..5fded39c511 100644
--- a/io.openems.edge.predictor.persistencemodel/bnd.bnd
+++ b/io.openems.edge.predictor.persistencemodel/bnd.bnd
@@ -1,15 +1,15 @@
-Bundle-Name: OpenEMS Edge Predictor Persistence Model
+Bundle-Name: OpenEMS Edge Predictor Persistence-Model
Bundle-Vendor: FENECON GmbH
Bundle-License: https://opensource.org/licenses/EPL-2.0
Bundle-Version: 1.0.0.${tstamp}
-Bundle-Description: \
- This Bundle describes the persistent model for predicting values.
-buildpath: \
${buildpath},\
io.openems.common,\
io.openems.edge.common,\
- io.openems.edge.predictor.api
+ io.openems.edge.controller.api,\
+ io.openems.edge.predictor.api,\
+ io.openems.edge.timedata.api
-testpath: \
${testpath}
diff --git a/io.openems.edge.predictor.persistencemodel/readme.adoc b/io.openems.edge.predictor.persistencemodel/readme.adoc
index 5f730cd320a..d95d7379713 100644
--- a/io.openems.edge.predictor.persistencemodel/readme.adoc
+++ b/io.openems.edge.predictor.persistencemodel/readme.adoc
@@ -1,3 +1,5 @@
-= Persistence Model Predictor
+= Persistence-Model Predictor
-Predicts values using the 'same-as-last-day' approach.
\ No newline at end of file
+Predicts values using the 'same-as-last-day' approach.
+
+https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.predictor.holtwinters[Source Code icon:github[]]
\ No newline at end of file
diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/AbstractPersistenceModelPredictor.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/AbstractPersistenceModelPredictor.java
similarity index 93%
rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/AbstractPersistenceModelPredictor.java
rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/AbstractPersistenceModelPredictor.java
index baec10c1de1..1498d66d2f5 100644
--- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/AbstractPersistenceModelPredictor.java
+++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/AbstractPersistenceModelPredictor.java
@@ -1,4 +1,4 @@
-package io.openems.edge.predictor.persistencemodel;
+package io.openems.edge.predictor.deprecatedpersistencemodel;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
@@ -18,9 +18,10 @@
import io.openems.edge.common.component.ComponentManager;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.event.EdgeEventConstants;
-import io.openems.edge.predictor.api.HourlyPrediction;
-import io.openems.edge.predictor.api.HourlyPredictor;
+import io.openems.edge.predictor.api.hourly.HourlyPrediction;
+import io.openems.edge.predictor.api.hourly.HourlyPredictor;
+//TODO remove the AbstractPersistenceModelPredictor in favor of PredictorManager API
public abstract class AbstractPersistenceModelPredictor extends AbstractOpenemsComponent implements HourlyPredictor {
private final Logger log = LoggerFactory.getLogger(AbstractPersistenceModelPredictor.class);
@@ -120,4 +121,4 @@ public HourlyPrediction get24hPrediction() {
return hourlyPrediction;
}
-}
+}
\ No newline at end of file
diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PredictorChannelId.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/PredictorChannelId.java
similarity index 85%
rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PredictorChannelId.java
rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/PredictorChannelId.java
index 54cd3f77ab6..ddac8dc056e 100644
--- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PredictorChannelId.java
+++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/PredictorChannelId.java
@@ -1,4 +1,4 @@
-package io.openems.edge.predictor.persistencemodel;
+package io.openems.edge.predictor.deprecatedpersistencemodel;
import io.openems.common.channel.Level;
import io.openems.edge.common.channel.Doc;
diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/Config.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/Config.java
similarity index 91%
rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/Config.java
rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/Config.java
index 954a80d7023..c34d2214e3e 100644
--- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/Config.java
+++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/Config.java
@@ -1,4 +1,4 @@
-package io.openems.edge.predictor.persistencemodel.consumption;
+package io.openems.edge.predictor.deprecatedpersistencemodel.consumption;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
@@ -18,4 +18,4 @@
boolean enabled() default true;
String webconsole_configurationFactory_nameHint() default "Predictor Consumption Persistence-Model [{id}]";
-}
+}
\ No newline at end of file
diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/ConsumptionPredictor.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/ConsumptionPredictor.java
similarity index 88%
rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/ConsumptionPredictor.java
rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/ConsumptionPredictor.java
index 7f2e86730cd..3c131bde9ad 100644
--- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/ConsumptionPredictor.java
+++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/ConsumptionPredictor.java
@@ -1,4 +1,4 @@
-package io.openems.edge.predictor.persistencemodel.consumption;
+package io.openems.edge.predictor.deprecatedpersistencemodel.consumption;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
@@ -16,8 +16,8 @@
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.event.EdgeEventConstants;
import io.openems.edge.common.sum.Sum;
-import io.openems.edge.predictor.api.ConsumptionHourlyPredictor;
-import io.openems.edge.predictor.persistencemodel.AbstractPersistenceModelPredictor;
+import io.openems.edge.predictor.api.hourly.ConsumptionHourlyPredictor;
+import io.openems.edge.predictor.deprecatedpersistencemodel.AbstractPersistenceModelPredictor;
@Designate(ocd = Config.class, factory = true)
@Component(name = "Predictor.Consumption.PersistenceModel", //
@@ -50,4 +50,4 @@ protected ComponentManager getComponentManager() {
return this.componentManager;
}
-}
+}
\ No newline at end of file
diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/Config.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/Config.java
similarity index 91%
rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/Config.java
rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/Config.java
index 86536d7e6a6..9bb4ed9246a 100644
--- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/Config.java
+++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/Config.java
@@ -1,4 +1,4 @@
-package io.openems.edge.predictor.persistencemodel.production;
+package io.openems.edge.predictor.deprecatedpersistencemodel.prediction;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
@@ -18,4 +18,4 @@
boolean enabled() default true;
String webconsole_configurationFactory_nameHint() default "Predictor Production Persistence-Model [{id}]";
-}
+}
\ No newline at end of file
diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/ProductionPredictor.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/ProductionPredictor.java
similarity index 88%
rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/ProductionPredictor.java
rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/ProductionPredictor.java
index 59e9697c24d..ca5180835a2 100644
--- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/ProductionPredictor.java
+++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/ProductionPredictor.java
@@ -1,4 +1,4 @@
-package io.openems.edge.predictor.persistencemodel.production;
+package io.openems.edge.predictor.deprecatedpersistencemodel.prediction;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
@@ -16,8 +16,8 @@
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.event.EdgeEventConstants;
import io.openems.edge.common.sum.Sum;
-import io.openems.edge.predictor.api.ProductionHourlyPredictor;
-import io.openems.edge.predictor.persistencemodel.AbstractPersistenceModelPredictor;
+import io.openems.edge.predictor.api.hourly.ProductionHourlyPredictor;
+import io.openems.edge.predictor.deprecatedpersistencemodel.AbstractPersistenceModelPredictor;
@Designate(ocd = Config.class, factory = true)
@Component(name = "Predictor.Production.PersistenceModel", //
@@ -49,4 +49,4 @@ protected void deactivate() {
protected ComponentManager getComponentManager() {
return this.componentManager;
}
-}
+}
\ No newline at end of file
diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/Config.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/Config.java
new file mode 100644
index 00000000000..3221812f9dc
--- /dev/null
+++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/Config.java
@@ -0,0 +1,26 @@
+package io.openems.edge.predictor.persistencemodel;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+@ObjectClassDefinition(//
+ name = "Predictor Persistence-Model", //
+ description = "")
+@interface Config {
+
+ @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component")
+ String id() default "predictor0";
+
+ @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 = "Channel-Addresses", description = "List of Channel-Addresses this Predictor is used for, e.g. '*/ActivePower', '*/ActualPower'")
+ // TODO "_sum/ConsumptionActivePower" holds also actively controlled consumption; replace, once we introduce a 'Sum-Non-Regulated-Consumption'-Channel
+ String[] channelAddresses() default { "_sum/ProductionActivePower", "_sum/ConsumptionActivePower" };
+
+ String webconsole_configurationFactory_nameHint() default "Predictor Persistence-Model [{id}]";
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictor.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictor.java
new file mode 100644
index 00000000000..c5fa7b04bf9
--- /dev/null
+++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictor.java
@@ -0,0 +1,24 @@
+package io.openems.edge.predictor.persistencemodel;
+
+import io.openems.edge.common.channel.Doc;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.predictor.api.oneday.Predictor24Hours;
+
+public interface PersistenceModelPredictor extends Predictor24Hours, OpenemsComponent {
+
+ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
+ ;
+
+ private final Doc doc;
+
+ private ChannelId(Doc doc) {
+ this.doc = doc;
+ }
+
+ @Override
+ public Doc doc() {
+ return this.doc;
+ }
+ }
+
+}
diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorImpl.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorImpl.java
new file mode 100644
index 00000000000..027e8677601
--- /dev/null
+++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorImpl.java
@@ -0,0 +1,107 @@
+package io.openems.edge.predictor.persistencemodel;
+
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.SortedMap;
+
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Sets;
+import com.google.gson.JsonElement;
+
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.common.types.ChannelAddress;
+import io.openems.edge.common.component.ClockProvider;
+import io.openems.edge.common.component.ComponentManager;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.controller.api.Controller;
+import io.openems.edge.predictor.api.oneday.AbstractPredictor24Hours;
+import io.openems.edge.predictor.api.oneday.Prediction24Hours;
+import io.openems.edge.predictor.api.oneday.Predictor24Hours;
+import io.openems.edge.timedata.api.Timedata;
+
+@Designate(ocd = Config.class, factory = true)
+@Component(//
+ name = "Predictor.PersistenceModel", //
+ immediate = true, //
+ configurationPolicy = ConfigurationPolicy.REQUIRE //
+)
+public class PersistenceModelPredictorImpl extends AbstractPredictor24Hours
+ implements Predictor24Hours, OpenemsComponent {
+
+ private final Logger log = LoggerFactory.getLogger(PersistenceModelPredictorImpl.class);
+
+ @Reference
+ private Timedata timedata;
+
+ @Reference
+ private ComponentManager componentManager;
+
+ public PersistenceModelPredictorImpl() throws OpenemsNamedException {
+ super(//
+ OpenemsComponent.ChannelId.values(), //
+ Controller.ChannelId.values(), //
+ PersistenceModelPredictor.ChannelId.values() //
+ );
+ }
+
+ @Activate
+ protected void activate(ComponentContext context, Config config) throws OpenemsNamedException {
+ super.activate(context, config.id(), config.alias(), config.enabled(), config.channelAddresses());
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ super.deactivate();
+ }
+
+ @Override
+ protected Prediction24Hours createNewPrediction(ChannelAddress channelAddress) {
+ ZonedDateTime now = ZonedDateTime.now(this.componentManager.getClock());
+ ZonedDateTime fromDate = now.minus(1, ChronoUnit.DAYS);
+
+ // Query database
+ final SortedMap> queryResult;
+ try {
+ queryResult = this.timedata.queryHistoricData(null, fromDate, now, Sets.newHashSet(channelAddress),
+ 900 /* seconds per 15 minutes */);
+ } catch (OpenemsNamedException e) {
+ this.logError(this.log, e.getMessage());
+ e.printStackTrace();
+ return Prediction24Hours.EMPTY;
+ }
+
+ // Extract data
+ Integer[] result = queryResult.values().stream() //
+ .map(m -> m.values()) //
+ // extract JsonElement values as flat stream
+ .flatMap(Collection::stream) //
+ // convert JsonElement to Integer
+ .map(v -> {
+ if (v.isJsonNull()) {
+ return (Integer) null;
+ } else {
+ return v.getAsInt();
+ }
+ })
+ // get as Array
+ .toArray(Integer[]::new);
+
+ return new Prediction24Hours(result);
+ }
+
+ @Override
+ protected ClockProvider getClockProvider() {
+ return this.componentManager;
+ }
+
+}
diff --git a/io.openems.edge.predictor.persistencemodel/test/.gitignore b/io.openems.edge.predictor.persistencemodel/test/.gitignore
index 90dde36e4ac..e69de29bb2d 100644
--- a/io.openems.edge.predictor.persistencemodel/test/.gitignore
+++ b/io.openems.edge.predictor.persistencemodel/test/.gitignore
@@ -1,3 +0,0 @@
-/bin/
-/bin_test/
-/generated/
diff --git a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/MyConfig.java b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/MyConfig.java
similarity index 63%
rename from io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/MyConfig.java
rename to io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/MyConfig.java
index 235aa4d43c7..2888f8200c1 100644
--- a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/MyConfig.java
+++ b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/MyConfig.java
@@ -1,12 +1,14 @@
-package io.openems.edge.predictor.persistencemodel.production;
+package io.openems.edge.predictor.persistencemodel;
import io.openems.edge.common.test.AbstractComponentConfig;
+import io.openems.edge.predictor.persistencemodel.Config;
@SuppressWarnings("all")
public class MyConfig extends AbstractComponentConfig implements Config {
protected static class Builder {
private String id;
+ public String[] channelAddresses;
private Builder() {
}
@@ -16,6 +18,11 @@ public Builder setId(String id) {
return this;
}
+ public Builder setChannelAddresses(String... channelAddresses) {
+ this.channelAddresses = channelAddresses;
+ return this;
+ }
+
public MyConfig build() {
return new MyConfig(this);
}
@@ -37,4 +44,9 @@ private MyConfig(Builder builder) {
this.builder = builder;
}
+ @Override
+ public String[] channelAddresses() {
+ return this.builder.channelAddresses;
+ }
+
}
\ No newline at end of file
diff --git a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorTest.java b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorTest.java
new file mode 100644
index 00000000000..9415e5b41b7
--- /dev/null
+++ b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorTest.java
@@ -0,0 +1,73 @@
+package io.openems.edge.predictor.persistencemodel;
+
+import static org.junit.Assert.assertEquals;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import io.openems.common.types.ChannelAddress;
+import io.openems.edge.common.test.ComponentTest;
+import io.openems.edge.common.test.DummyComponentManager;
+import io.openems.edge.common.test.TimeLeapClock;
+import io.openems.edge.predictor.api.oneday.Prediction24Hours;
+import io.openems.edge.timedata.test.DummyTimedata;
+
+public class PersistenceModelPredictorTest {
+
+ private static final String TIMEDATA_ID = "timedata0";
+ private static final String PREDICTOR_ID = "predictor0";
+
+ private static final ChannelAddress METER1_ACTIVE_POWER = new ChannelAddress("meter1", "ActivePower");
+
+ @Test
+ public void test() throws Exception {
+ final TimeLeapClock clock = new TimeLeapClock(
+ Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, ZoneOffset.UTC);
+ int[] values = {
+ // Day 1
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 9, 146, 348, 636, 1192, 2092, 2882, 3181,
+ 3850, 5169, 6005, 6710, 7372, 8138, 8918, 9736, 10615, 11281, 11898, 12435, 11982, 14287, 15568, 16747,
+ 16934, 17221, 17573, 15065, 16726, 16670, 16696, 16477, 16750, 16991, 17132, 17567, 17003, 17686, 17753,
+ 17773, 17381, 17059, 17110, 16395, 15803, 15044, 14413, 13075, 12975, 6748, 7845, 10781, 8605, 6202,
+ 3049, 1697, 1184, 1142, 1015, 568, 1093, 414, 121, 110, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ // Day 2
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 146, 297, 489, 1111, 1953, 3825,
+ 2346, 3356, 3407, 3482, 4238, 7179, 11642, 5486, 4265, 5488, 5559, 6589, 7608, 9285, 7668, 6077, 3918,
+ 4498, 7221, 9628, 11962, 9483, 11746, 10401, 8875, 8825, 13945, 16488, 13038, 17702, 16772, 7319, 228,
+ 477, 501, 547, 589, 1067, 13304, 17367, 14825, 13654, 12545, 8371, 10468, 9810, 8537, 6228, 3758, 4131,
+ 3572, 1698, 1017, 569, 188, 14, 2, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+
+ DummyTimedata timedata = new DummyTimedata(TIMEDATA_ID);
+ ZonedDateTime start = ZonedDateTime.of(2019, 12, 30, 0, 0, 0, 0, ZoneId.of("UTC"));
+ for (int i = 0; i < values.length; i++) {
+ timedata.add(start.plusMinutes(i * 15), METER1_ACTIVE_POWER, values[i]);
+ }
+
+ PersistenceModelPredictorImpl sut = new PersistenceModelPredictorImpl();
+
+ new ComponentTest(sut) //
+ .addReference("timedata", timedata) //
+ .addReference("componentManager", new DummyComponentManager(clock)) //
+ .activate(MyConfig.create() //
+ .setId(PREDICTOR_ID) //
+ .setChannelAddresses(METER1_ACTIVE_POWER.toString()) //
+ .build());
+
+ Prediction24Hours prediction = sut.get24HoursPrediction(METER1_ACTIVE_POWER);
+ Integer[] p = prediction.getValues();
+
+ assertEquals((Integer) 0, p[0]);
+ assertEquals((Integer) 3, p[20]);
+ assertEquals((Integer) 6, p[21]);
+ assertEquals((Integer) 146, p[22]);
+ assertEquals((Integer) 297, p[23]);
+
+ System.out.println(Arrays.toString(prediction.getValues()));
+ }
+
+}
diff --git a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/ProductionPredictorTest.java b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/ProductionPredictorTest.java
deleted file mode 100644
index bf0214dcc03..00000000000
--- a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/ProductionPredictorTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package io.openems.edge.predictor.persistencemodel.production;
-
-import static org.junit.Assert.assertEquals;
-
-import java.time.ZonedDateTime;
-import java.time.temporal.ChronoUnit;
-
-import org.junit.Test;
-
-import io.openems.common.types.ChannelAddress;
-import io.openems.edge.common.sum.DummySum;
-import io.openems.edge.common.test.AbstractComponentTest.TestCase;
-import io.openems.edge.common.test.ComponentTest;
-import io.openems.edge.common.test.DummyComponentManager;
-import io.openems.edge.common.test.TimeLeapClock;
-import io.openems.edge.predictor.api.HourlyPrediction;
-
-public class ProductionPredictorTest {
-
- private static final String CTRL_ID = "ctrl0";
-
- private static final String SUM_ID = "_sum";
- private static final ChannelAddress SUM_PRODUCTION_ACTIVE_ENERGY = new ChannelAddress(SUM_ID,
- "ProductionActiveEnergy");
-
- @Test
- public void test() throws Exception {
- final TimeLeapClock clock = new TimeLeapClock();
- final ProductionPredictor predictor = new ProductionPredictor();
- new ComponentTest(predictor) //
- .addReference("componentManager", new DummyComponentManager(clock)) //
- .addComponent(new DummySum()) //
- .activate(MyConfig.create() //
- .setId(CTRL_ID) //
- .build())
- .next(new TestCase() //
- .input(SUM_PRODUCTION_ACTIVE_ENERGY, 1000))
- .next(new TestCase() //
- .timeleap(clock, 1, ChronoUnit.MINUTES) //
- .input(SUM_PRODUCTION_ACTIVE_ENERGY, 1100))
- .next(new TestCase() //
- .timeleap(clock, 1, ChronoUnit.HOURS) //
- .input(SUM_PRODUCTION_ACTIVE_ENERGY, 2000))
- .next(new TestCase() //
- .timeleap(clock, 1, ChronoUnit.HOURS) //
- .input(SUM_PRODUCTION_ACTIVE_ENERGY, 4000))
- .next(new TestCase() //
- .timeleap(clock, 1, ChronoUnit.HOURS) //
- .input(SUM_PRODUCTION_ACTIVE_ENERGY, 5500));
-
- HourlyPrediction p = predictor.get24hPrediction();
- assertEquals(p.getStart(), ZonedDateTime.now(clock).withNano(0).withMinute(0).withSecond(0));
-
- Integer[] v = p.getValues();
- assertEquals(v.length, 24);
-
- assertEquals(null, v[0]);
- assertEquals(null, v[1]);
- assertEquals(null, v[2]);
- assertEquals(null, v[3]);
- assertEquals(null, v[4]);
- assertEquals(null, v[5]);
- assertEquals(null, v[6]);
- assertEquals(null, v[7]);
- assertEquals(null, v[8]);
- assertEquals(null, v[9]);
- assertEquals(null, v[10]);
- assertEquals(null, v[11]);
- assertEquals(null, v[12]);
- assertEquals(null, v[13]);
- assertEquals(null, v[14]);
- assertEquals(null, v[15]);
- assertEquals(null, v[16]);
- assertEquals(null, v[17]);
- assertEquals(null, v[18]);
- assertEquals(null, v[19]);
- assertEquals(null, v[20]);
- assertEquals(Integer.valueOf(1000), v[21]);
- assertEquals(Integer.valueOf(2000), v[22]);
- assertEquals(Integer.valueOf(1500), v[23]);
- }
-
-}
diff --git a/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/Scheduler.java b/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/Scheduler.java
index d2f1b55a587..a6109be9dcf 100644
--- a/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/Scheduler.java
+++ b/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/Scheduler.java
@@ -5,7 +5,6 @@
import org.osgi.annotation.versioning.ProviderType;
import io.openems.common.channel.Level;
-import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.edge.common.channel.Doc;
import io.openems.edge.common.channel.StateChannel;
import io.openems.edge.common.channel.value.Value;
@@ -67,7 +66,6 @@ public default void _setControllerIsMissing(boolean value) {
* {@link LinkedHashSet} is used, as it preserves insertion order
*
* @return a ordered set of Component-IDs of Controllers
- * @throws OpenemsNamedException on error
*/
public LinkedHashSet getControllers();
diff --git a/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/DailySchedulerImplTest.java b/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/DailySchedulerImplTest.java
index 54d1ea27ad9..a504b4f0668 100644
--- a/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/DailySchedulerImplTest.java
+++ b/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/DailySchedulerImplTest.java
@@ -34,7 +34,7 @@ public class DailySchedulerImplTest {
public void test() throws Exception {
final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T00:00:00.00Z"), ZoneOffset.UTC);
final DailyScheduler sut = new DailySchedulerImpl();
- ComponentTest test = new ComponentTest(sut) //
+ new ComponentTest(sut) //
.addReference("componentManager", new DummyComponentManager(clock)) //
.addComponent(new DummyController(CTRL0_ID)) //
.addComponent(new DummyController(CTRL1_ID)) //
@@ -58,24 +58,21 @@ public void test() throws Exception {
.build()) //
.build().toString())
.setAlwaysRunAfterControllerIds(CTRL3_ID, CTRL1_ID) //
- .build()); //
-
- test.next(new TestCase("00:00")); //
- assertEquals(//
- Arrays.asList(CTRL2_ID, CTRL4_ID, CTRL3_ID, CTRL1_ID), //
- getControllerIds(sut));
-
- test.next(new TestCase("12:00") //
- .timeleap(clock, 12, ChronoUnit.HOURS)); //
- assertEquals(//
- Arrays.asList(CTRL2_ID, CTRL0_ID, CTRL3_ID, CTRL1_ID), //
- getControllerIds(sut));
-
- test.next(new TestCase("14:00") //
- .timeleap(clock, 12, ChronoUnit.HOURS)); //
- assertEquals(//
- Arrays.asList(CTRL2_ID, CTRL4_ID, CTRL3_ID, CTRL1_ID), //
- getControllerIds(sut));
+ .build()) //
+ .next(new TestCase("00:00") //
+ .onBeforeControllersCallbacks(() -> assertEquals(//
+ Arrays.asList(CTRL2_ID, CTRL4_ID, CTRL3_ID, CTRL1_ID), //
+ getControllerIds(sut)))) //
+ .next(new TestCase("12:00") //
+ .timeleap(clock, 12, ChronoUnit.HOURS) //
+ .onBeforeControllersCallbacks(() -> assertEquals(//
+ Arrays.asList(CTRL2_ID, CTRL0_ID, CTRL3_ID, CTRL1_ID), //
+ getControllerIds(sut))))
+ .next(new TestCase("14:00") //
+ .timeleap(clock, 12, ChronoUnit.HOURS) //
+ .onBeforeControllersCallbacks(() -> assertEquals(//
+ Arrays.asList(CTRL2_ID, CTRL4_ID, CTRL3_ID, CTRL1_ID), //
+ getControllerIds(sut))));
}
private static List getControllerIds(Scheduler scheduler) throws OpenemsNamedException {
diff --git a/io.openems.edge.simulator/readme.adoc b/io.openems.edge.simulator/readme.adoc
index ee25baf71a0..f09aacea8b8 100644
--- a/io.openems.edge.simulator/readme.adoc
+++ b/io.openems.edge.simulator/readme.adoc
@@ -9,21 +9,22 @@ The Simulator-App is a very specific component that needs to be handled with car
CAUTION: Be aware that the SimulatorApp Component takes control over the complete OpenEMS Edge Application, i.e. if you enable it, it is going to *delete all existing Component configurations*!
To run a simulation:
-- Run OpenEMS Edge using the EdgeApp.bndrun
-- Configure a read-write JSON/REST Api
-- Send a https://openems.github.io/openems.io/openems/latest/edge/controller.html#_endpoint_jsonrpc[JSON-RPC Request] like the following, providing full configurations for all required OpenEMS Edge Components
+
+. Run OpenEMS Edge using the EdgeApp.bndrun
+. Open up Apache Felix Web Console and
+
+.. activate a "Controller Api REST/JSON Read-Write"
+.. activate a "Simulator App"
+
+. Send a https://openems.github.io/openems.io/openems/latest/edge/controller.html#_endpoint_jsonrpc[JSON-RPC Request] like the following, providing full configurations for all required OpenEMS Edge Components
[source,json]
----
{
- "jsonrpc":"2.0",
- "id":"7132233f-1ca3-1eb3-8800-86d246d47c1d",
"method":"componentJsonApi",
"params":{
"componentId":"_simulator",
"payload":{
- "jsonrpc":"2.0",
- "id":"addccd39-1bac-89c2-91ac-13bf5b1e9743",
"method":"executeSimulation",
"params":{
"components":[
@@ -54,7 +55,7 @@ To run a simulation:
},
{
"name":"alias",
- "value":"Verbrauch"
+ "value":"Consumption"
},
{
"name":"datasource.id",
@@ -71,7 +72,7 @@ To run a simulation:
},
{
"name":"alias",
- "value":"S�ddach"
+ "value":"South Roof"
},
{
"name":"datasource.id",
diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorApp.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorApp.java
index fc142898cae..ee7f4e5a84a 100644
--- a/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorApp.java
+++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorApp.java
@@ -12,6 +12,7 @@
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
+import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
@@ -331,7 +332,9 @@ private void deleteAllConfigurations(User user) throws OpenemsNamedException {
if (factoryPid == null || factoryPid.trim().isEmpty()) {
continue;
}
- if (factoryPid.startsWith("Core.") || factoryPid.startsWith("Controller.Api.")) {
+ if (factoryPid.startsWith("Core.") //
+ || factoryPid.startsWith("Controller.Api.") //
+ || factoryPid.startsWith("Predictor.")) {
continue;
}
switch (factoryPid) {
@@ -482,10 +485,31 @@ public SortedMap> queryHis
ZonedDateTime fromDate, ZonedDateTime toDate, Set channels, int resolution)
throws OpenemsNamedException {
if (this.lastSimulation == null || this.lastSimulation.collectedData.isEmpty()) {
+ // return empty result
return new TreeMap<>();
}
+
Period fakePeriod = this.convertToSimulatedFromToDates(fromDate, toDate);
- return this.lastSimulation.collectedData.subMap(fakePeriod.fromDate, fakePeriod.toDate);
+ SortedMap> data = this.lastSimulation.collectedData
+ .subMap(fakePeriod.fromDate, fakePeriod.toDate);
+
+ if (channels.isEmpty()) {
+ // No Channels given -> return all data
+ return data;
+ }
+
+ SortedMap> result = new TreeMap<>();
+ for (Entry> entry : this.lastSimulation.collectedData
+ .subMap(fakePeriod.fromDate, fakePeriod.toDate).entrySet()) {
+ SortedMap values = entry.getValue();
+ TreeMap resultPerTimestamp = new TreeMap<>();
+ for (ChannelAddress channel : channels) {
+ JsonElement value = values.get(channel);
+ resultPerTimestamp.put(channel, value == null ? JsonNull.INSTANCE : value);
+ }
+ result.put(entry.getKey(), resultPerTimestamp);
+ }
+ return result;
}
@Override
diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/battery/BatteryDummy.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/battery/BatteryDummy.java
index d6ee1d6eae0..faf571cce9b 100644
--- a/io.openems.edge.simulator/src/io/openems/edge/simulator/battery/BatteryDummy.java
+++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/battery/BatteryDummy.java
@@ -42,7 +42,8 @@ public class BatteryDummy extends AbstractOpenemsComponent
public BatteryDummy() {
super(//
OpenemsComponent.ChannelId.values(), //
- Battery.ChannelId.values() //
+ Battery.ChannelId.values(), //
+ StartStoppable.ChannelId.values() //
);
}
diff --git a/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2Core.java b/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2Core.java
index 58cc34d7e07..a5de7409bd3 100644
--- a/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2Core.java
+++ b/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2Core.java
@@ -15,12 +15,12 @@ public interface TeslaPowerwall2Core extends OpenemsComponent {
public Optional getBattery();
- public enum CoreChannelId implements io.openems.edge.common.channel.ChannelId {
+ public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
SLAVE_COMMUNICATION_FAILED(Doc.of(Level.FAULT));
private final Doc doc;
- private CoreChannelId(Doc doc) {
+ private ChannelId(Doc doc) {
this.doc = doc;
}
@@ -36,7 +36,7 @@ public Doc doc() {
* @return the Channel
*/
public default StateChannel getSlaveCommunicationFailedChannel() {
- return this.channel(CoreChannelId.SLAVE_COMMUNICATION_FAILED);
+ return this.channel(ChannelId.SLAVE_COMMUNICATION_FAILED);
}
/**
diff --git a/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImpl.java b/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImpl.java
index 495447127c3..31d386569dc 100644
--- a/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImpl.java
+++ b/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImpl.java
@@ -40,7 +40,7 @@ public class TeslaPowerwall2CoreImpl extends AbstractOpenemsComponent
public TeslaPowerwall2CoreImpl() {
super(//
OpenemsComponent.ChannelId.values(), //
- CoreChannelId.values() //
+ TeslaPowerwall2Core.ChannelId.values() //
);
}
diff --git a/io.openems.edge.timedata.api/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.timedata.api/.settings/org.eclipse.core.resources.prefs
index ecd059818a9..fd04b997d93 100644
--- a/io.openems.edge.timedata.api/.settings/org.eclipse.core.resources.prefs
+++ b/io.openems.edge.timedata.api/.settings/org.eclipse.core.resources.prefs
@@ -1,5 +1,6 @@
eclipse.preferences.version=1
encoding//src/io/openems/edge/timedata/api/Timedata.java=UTF-8
encoding//src/io/openems/edge/timedata/api/package-info.java=UTF-8
+encoding//src/io/openems/edge/timedata/test/package-info.java=UTF-8
encoding//test/.gitignore=UTF-8
encoding/bnd.bnd=UTF-8
diff --git a/io.openems.edge.timedata.api/src/io/openems/edge/timedata/api/Timedata.java b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/api/Timedata.java
index eb48fdd9615..b4bc7737ff0 100644
--- a/io.openems.edge.timedata.api/src/io/openems/edge/timedata/api/Timedata.java
+++ b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/api/Timedata.java
@@ -5,7 +5,6 @@
import org.osgi.annotation.versioning.ProviderType;
-import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.timedata.CommonTimedataService;
import io.openems.common.types.ChannelAddress;
import io.openems.edge.common.channel.Doc;
@@ -33,7 +32,6 @@ public Doc doc() {
*
* @param channelAddress the ChannelAddress to be queried
* @return the latest known value or Empty
- * @throws OpenemsNamedException on error
*/
public CompletableFuture> getLatestValue(ChannelAddress channelAddress);
diff --git a/io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/DummyTimedata.java b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/DummyTimedata.java
new file mode 100644
index 00000000000..0677c1908d3
--- /dev/null
+++ b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/DummyTimedata.java
@@ -0,0 +1,105 @@
+package io.openems.edge.timedata.test;
+
+import java.time.ZonedDateTime;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.CompletableFuture;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+
+import io.openems.common.exceptions.NotImplementedException;
+import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
+import io.openems.common.types.ChannelAddress;
+import io.openems.edge.common.channel.Channel;
+import io.openems.edge.common.component.AbstractOpenemsComponent;
+import io.openems.edge.common.component.OpenemsComponent;
+import io.openems.edge.timedata.api.Timedata;
+
+/**
+ * Provides a simple, simulated {@link Timedata} component that can be used
+ * together with the OpenEMS Component test framework.
+ */
+public class DummyTimedata extends AbstractOpenemsComponent implements Timedata {
+
+ private final SortedMap> data = new TreeMap<>();
+
+ public DummyTimedata(String id) {
+ super(//
+ OpenemsComponent.ChannelId.values(), //
+ Timedata.ChannelId.values() //
+ );
+ for (Channel> channel : this.channels()) {
+ channel.nextProcessImage();
+ }
+ super.activate(null, id, "", true);
+ }
+
+ /**
+ * Adds a value to the Dummy Timedata.
+ *
+ * @param timestamp the {@link ZonedDateTime}
+ * @param channelAddress the {@link ChannelAddress}
+ * @param value the value as {@link Integer}
+ */
+ public void add(ZonedDateTime timestamp, ChannelAddress channelAddress, Integer value) {
+ this.add(timestamp, channelAddress, new JsonPrimitive(value));
+ }
+
+ /**
+ * Adds a value to the Dummy Timedata.
+ *
+ * @param timestamp the {@link ZonedDateTime}
+ * @param channelAddress the {@link ChannelAddress}
+ * @param value the value as {@link JsonElement}
+ */
+ public void add(ZonedDateTime timestamp, ChannelAddress channelAddress, JsonElement value) {
+ SortedMap perTime = this.data.get(timestamp);
+ if (perTime == null) {
+ perTime = new TreeMap<>();
+ this.data.put(timestamp, perTime);
+ }
+ perTime.put(channelAddress, value);
+ }
+
+ @Override
+ public SortedMap> queryHistoricData(String edgeId,
+ ZonedDateTime fromDate, ZonedDateTime toDate, Set channels, int resolution)
+ throws OpenemsNamedException {
+ SortedMap> result = new TreeMap<>();
+ for (Entry> entry : this.data.subMap(fromDate, toDate)
+ .entrySet()) {
+ SortedMap subResult = new TreeMap<>();
+ for (ChannelAddress channelAddress : channels) {
+ subResult.put(channelAddress, entry.getValue().get(channelAddress));
+ }
+ result.put(entry.getKey(), subResult);
+ }
+ return result;
+ }
+
+ @Override
+ public SortedMap queryHistoricEnergy(String edgeId, ZonedDateTime fromDate,
+ ZonedDateTime toDate, Set channels) throws OpenemsNamedException {
+ // TODO Auto-generated method stub
+ throw new NotImplementedException("DummyTimedata.queryHistoricEnergy() is not implemented");
+ }
+
+ @Override
+ public SortedMap> queryHistoricEnergyPerPeriod(String edgeId,
+ ZonedDateTime fromDate, ZonedDateTime toDate, Set channels, int resolution)
+ throws OpenemsNamedException {
+ // TODO Auto-generated method stub
+ throw new NotImplementedException("DummyTimedata.queryHistoricEnergyPerPeriod() is not implemented");
+ }
+
+ @Override
+ public CompletableFuture> getLatestValue(ChannelAddress channelAddress) {
+ // TODO Auto-generated method stub
+ throw new IllegalArgumentException("DummyTimedata.getLatestValue() is not implemented");
+ }
+
+}
diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/package-info.java b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/package-info.java
similarity index 68%
rename from io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/package-info.java
rename to io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/package-info.java
index 5eae92ae19c..fc2211373df 100644
--- a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/package-info.java
+++ b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/package-info.java
@@ -1,3 +1,3 @@
@org.osgi.annotation.versioning.Version("1.0.0")
@org.osgi.annotation.bundle.Export
-package io.openems.edge.predictor.api;
+package io.openems.edge.timedata.test;
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 87094f854ab..6d32e546cdd 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
@@ -50,6 +50,7 @@
import io.openems.edge.common.component.ComponentManager;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.event.EdgeEventConstants;
+import io.openems.edge.common.type.TypeUtils;
import io.openems.edge.timedata.api.Timedata;
@Designate(ocd = Config.class, factory = true)
@@ -60,10 +61,11 @@
public class Rrd4jTimedataImpl extends AbstractOpenemsComponent
implements Rrd4jTimedata, Timedata, OpenemsComponent, EventHandler {
+ protected static final String DEFAULT_DATASOURCE_NAME = "value";
+ protected static final int DEFAULT_STEP_SECONDS = 60;
+ protected static final int DEFAULT_HEARTBEAT_SECONDS = DEFAULT_STEP_SECONDS;
+
private static final String RRD4J_PATH = "rrd4j";
- private static final String DEFAULT_DATASOURCE_NAME = "value";
- private static final int DEFAULT_STEP_SECONDS = 60;
- private static final int DEFAULT_HEARTBEAT_SECONDS = DEFAULT_STEP_SECONDS;
private final Logger log = LoggerFactory.getLogger(Rrd4jTimedataImpl.class);
@@ -112,6 +114,7 @@ public SortedMap> queryHis
long toTimeStamp = toDate.withZoneSameInstant(ZoneOffset.UTC).toEpochSecond();
for (ChannelAddress channelAddress : channels) {
+
Channel> channel = this.componentManager.getChannel(channelAddress);
database = this.getExistingRrdDb(channel.address());
if (database == null) {
@@ -121,23 +124,30 @@ public SortedMap> queryHis
ChannelDef chDef = this.getDsDefForChannel(channel.channelDoc().getUnit());
FetchRequest request = database.createFetchRequest(chDef.consolFun, fromTimestamp, toTimeStamp,
resolution);
- FetchData data = request.fetchData();
+
+ // Post-Process data
+ double[] result = postProcessData(request, resolution);
database.close();
- for (int i = 0; i < data.getTimestamps().length; i++) {
- Instant timestampInstant = Instant.ofEpochSecond(data.getTimestamps()[i]);
+ for (int i = 0; i < result.length; i++) {
+ long timestamp = fromTimestamp + (i * resolution);
+
+ // Prepare result table row
+ Instant timestampInstant = Instant.ofEpochSecond(timestamp);
ZonedDateTime dateTime = ZonedDateTime.ofInstant(timestampInstant, ZoneOffset.UTC)
.withZoneSameInstant(timezone);
SortedMap tableRow = table.get(dateTime);
if (tableRow == null) {
tableRow = new TreeMap<>();
}
- double value = data.getValues(0)[i];
+
+ double value = result[i];
if (Double.isNaN(value)) {
tableRow.put(channelAddress, JsonNull.INSTANCE);
} else {
tableRow.put(channelAddress, new JsonPrimitive(value));
}
+
table.put(dateTime, tableRow);
}
}
@@ -155,6 +165,77 @@ public SortedMap> queryHis
return table;
}
+ /**
+ * Post-Process the received data.
+ *
+ *
+ * This mainly makes sure the data has the correct resolution.
+ *
+ * @param request the RRD4j {@link FetchRequest}
+ * @param resolution the resolution in seconds
+ * @return the result array
+ * @throws IOException on error
+ * @throws IllegalArgumentException on error
+ */
+ protected static double[] postProcessData(FetchRequest request, int resolution)
+ throws IOException, IllegalArgumentException {
+ FetchData data = request.fetchData();
+ long step = data.getStep();
+ double[] input = data.getValues()[0];
+
+ // Initialize result array
+ final double[] result = new double[(int) ((request.getFetchEnd() - request.getFetchStart()) / resolution)];
+ for (int i = 0; i < result.length; i++) {
+ result[i] = Double.NaN;
+ }
+
+ if (step < resolution) {
+ // Merge multiple entries to resolution
+ if (resolution % step != 0) {
+ throw new IllegalArgumentException(
+ "Requested resolution [" + resolution + "] is not dividable by RRD4j Step [" + step + "]");
+ }
+ int merge = (int) (resolution / step);
+ double[] buffer = new double[merge];
+ for (int i = 1; i < input.length; i += merge) {
+ for (int j = 0; j < merge; j++) {
+ if (i + j < input.length) {
+ buffer[j] = input[i + j];
+ } else {
+ buffer[j] = Double.NaN;
+ }
+ }
+
+ // put in result; avoid index rounding error
+ int resultIndex = (i - 1) / merge;
+ if (resultIndex >= result.length) {
+ break;
+ }
+ result[resultIndex] = TypeUtils.average(buffer);
+ }
+
+ } else if (step > resolution) {
+ // Split each entry to multiple values
+ if (step % resolution != 0) {
+ throw new IllegalArgumentException(
+ "RRD4j Step [" + step + "] is not dividable by requested resolution [" + resolution + "]");
+ }
+ int split = (int) (step / resolution);
+ for (int i = 1; i < input.length; i++) {
+ for (int j = 0; j < split; j++) {
+ result[(i - 1) * split + j] = input[i];
+ }
+ }
+
+ } else {
+ // Data already matches resolution
+ for (int i = 1; i < input.length; i++) {
+ result[i - 1] = input[i];
+ }
+ }
+ return result;
+ }
+
@Override
public SortedMap queryHistoricEnergy(String edgeId, ZonedDateTime fromDate,
ZonedDateTime toDate, Set channels) throws OpenemsNamedException {
diff --git a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImplTest.java b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImplTest.java
new file mode 100644
index 00000000000..412b7f9fe5b
--- /dev/null
+++ b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImplTest.java
@@ -0,0 +1,119 @@
+package io.openems.edge.timedata.rrd4j;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import org.junit.Test;
+import org.rrd4j.ConsolFun;
+import org.rrd4j.DsType;
+import org.rrd4j.core.DsDef;
+import org.rrd4j.core.FetchRequest;
+import org.rrd4j.core.RrdDb;
+import org.rrd4j.core.RrdDef;
+import org.rrd4j.core.RrdMemoryBackendFactory;
+import org.rrd4j.core.Sample;
+
+public class Rrd4jTimedataImplTest {
+
+ private final static Instant START = Instant.ofEpochSecond(1577836800L); /* starts at 1. January 2020 00:00:00 */
+
+ private static void addSample(RrdDb database, Instant instant, double value) throws IOException {
+ Sample sample = database.createSample(instant.getEpochSecond());
+ sample.setValue(0, value);
+ sample.update();
+ }
+
+ private static RrdDb createRrdDb(int oneMinute, int fiveMinutes) throws IOException, URISyntaxException {
+ final RrdDef rrdDef = new RrdDef("empty-path", START.getEpochSecond(), Rrd4jTimedataImpl.DEFAULT_STEP_SECONDS);
+ rrdDef.addDatasource(//
+ new DsDef(Rrd4jTimedataImpl.DEFAULT_DATASOURCE_NAME, //
+ DsType.GAUGE, //
+ Rrd4jTimedataImpl.DEFAULT_HEARTBEAT_SECONDS, // Heartbeat in [s], default 60 = 1 minute
+ Double.NaN, Double.NaN));
+ // detailed recordings
+ rrdDef.addArchive(ConsolFun.AVERAGE, 0.5, 1, oneMinute); // 1 step (1 minute), 1440 rows (1 day)
+ rrdDef.addArchive(ConsolFun.AVERAGE, 0.5, 5, fiveMinutes); // 5 steps (5 minutes), 2880 rows (10 days)
+ // hourly values for a very long time
+ rrdDef.addArchive(ConsolFun.AVERAGE, 0.5, 60, 87_600); // 60 steps (1 hour), 87600 rows (10 years)
+ final RrdDb database = RrdDb.getBuilder() //
+ .setBackendFactory(new RrdMemoryBackendFactory()) // in memory
+ .setRrdDef(rrdDef) //
+ .build();
+
+ for (int i = 1; i <= 120; i++) {
+ addSample(database, START.plus(i, ChronoUnit.MINUTES), i);
+ }
+ return database;
+ }
+
+ /**
+ * Test RRD4j step smaller than resolution.
+ *
+ * @throws IOException on error
+ * @throws URISyntaxException on error
+ */
+ @Test
+ public void testMerge() throws IOException, URISyntaxException {
+ int resolution = 300; // 5 minutes
+
+ RrdDb database = createRrdDb(1000, 2000);
+ FetchRequest request = database.createFetchRequest(ConsolFun.AVERAGE, START.getEpochSecond(),
+ START.plus(3, ChronoUnit.HOURS).getEpochSecond());
+ double[] result = Rrd4jTimedataImpl.postProcessData(request, resolution);
+ database.close();
+
+ assertEquals(36, result.length); // 3 hours * 12 entries/per hour (5 minutes) = 36
+ assertEquals(3.0, result[0], 0.001);
+ assertEquals(8.0, result[1], 0.001);
+ assertEquals(13.0, result[2], 0.001);
+ }
+
+ /**
+ * Test RRD4j step equals resolution.
+ *
+ * @throws IOException on error
+ * @throws URISyntaxException on error
+ */
+ @Test
+ public void testExact() throws IOException, URISyntaxException {
+ int resolution = 300; // 5 minutes
+
+ RrdDb database = createRrdDb(10, 200);
+ FetchRequest request = database.createFetchRequest(ConsolFun.AVERAGE, START.getEpochSecond(),
+ START.plus(3, ChronoUnit.HOURS).getEpochSecond());
+ double[] result = Rrd4jTimedataImpl.postProcessData(request, resolution);
+ database.close();
+
+ assertEquals(36, result.length); // 3 hours * 12 entries/per hour (5 minutes) = 36
+ assertEquals(3.0, result[0], 0.001);
+ assertEquals(8.0, result[1], 0.001);
+ assertEquals(13.0, result[2], 0.001);
+ }
+
+ /**
+ * Test RRD4j step bigger than resolution.
+ *
+ * @throws IOException on error
+ * @throws URISyntaxException on error
+ */
+ @Test
+ public void testSplit() throws IOException, URISyntaxException {
+ int resolution = 300; // 5 minutes
+
+ RrdDb database = createRrdDb(10, 20);
+ FetchRequest request = database.createFetchRequest(ConsolFun.AVERAGE, START.getEpochSecond(),
+ START.plus(3, ChronoUnit.HOURS).getEpochSecond());
+ double[] result = Rrd4jTimedataImpl.postProcessData(request, resolution);
+ database.close();
+
+ assertEquals(36, result.length); // 3 hours * 12 entries/per hour (5 minutes) = 36
+ assertEquals(30.5, result[0], 0.001);
+ assertEquals(30.5, result[1], 0.001);
+ assertEquals(30.5, result[2], 0.001);
+ assertEquals(90.5, result[12], 0.001);
+ }
+}
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 ad9506a409b..2d8f08f5f16 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
@@ -65,7 +65,7 @@ public class InfluxConnector {
* @param isReadOnly If true, a 'Read-Only-Mode' is activated, where no data
* is actually written to the database
* @param onWriteError A callback for write-errors, i.e. '(failedPoints,
- * throwable) -> {}'
+ * throwable) -> {}'
*/
public InfluxConnector(String ip, int port, String username, String password, String database,
String retentionPolicy, boolean isReadOnly, BiConsumer, Throwable> onWriteError) {
diff --git a/ui/package-lock.json b/ui/package-lock.json
index c60cdbd2bdb..9a66636b75d 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "openems-ui",
- "version": "2021.3.0",
+ "version": "2021.4.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3039,25 +3039,25 @@
}
},
"@ionic-native/core": {
- "version": "5.30.0",
- "resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-5.30.0.tgz",
- "integrity": "sha512-UkktFoSOAt/lgsc1nxnwjCul29yD06qHNjyv7/K7JxhqeJrqPBKihnkLu7OTAe52KdFBozRxLKDP6HWcGderqA==",
+ "version": "5.31.1",
+ "resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-5.31.1.tgz",
+ "integrity": "sha512-dbJHezSuY8OqyFwyQiS+5QscA/BONhWitXgniljEblC5kQeLOCe+8p30JYHXj9xDciYzfqFP8ICmyaGOqUHJYw==",
"requires": {
"@types/cordova": "^0.0.34"
}
},
"@ionic-native/splash-screen": {
- "version": "5.30.0",
- "resolved": "https://registry.npmjs.org/@ionic-native/splash-screen/-/splash-screen-5.30.0.tgz",
- "integrity": "sha512-QlVPuPqJeb4fkxEJ2M0tgUxcjSOPGDLqrFtgnosun6ZuYF8RBlnNu79/h9pa0jcTpq83U53zng/T5qw0M/ccTA==",
+ "version": "5.31.1",
+ "resolved": "https://registry.npmjs.org/@ionic-native/splash-screen/-/splash-screen-5.31.1.tgz",
+ "integrity": "sha512-Hcy1cMjWLnFE0TrIhpcNwld39dFipOQE63XpKuEhSJXfix1hibrC+0Nc3jEn0zBJUbbAHVJph6s9dohUxRycqg==",
"requires": {
"@types/cordova": "^0.0.34"
}
},
"@ionic-native/status-bar": {
- "version": "5.30.0",
- "resolved": "https://registry.npmjs.org/@ionic-native/status-bar/-/status-bar-5.30.0.tgz",
- "integrity": "sha512-AQglp5M5E3QN/aA3ERl76Fajpb1G2Zvnk4sJOo4ra5uRlw2lZ6E36DdpNo/NO/7ALxaddD9qXO/LCmvU72Obsg==",
+ "version": "5.31.1",
+ "resolved": "https://registry.npmjs.org/@ionic-native/status-bar/-/status-bar-5.31.1.tgz",
+ "integrity": "sha512-o5gugiuyYjWqQqzajbfqYKvuWX0BSTppwoLXU0PidgvBWtw3yBb7z4FZoo6JQSkUVn2AWYD1XEj/4KKh9D4Pkg==",
"requires": {
"@types/cordova": "^0.0.34"
}
@@ -3133,17 +3133,17 @@
}
},
"@ngx-formly/core": {
- "version": "5.10.13",
- "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-5.10.13.tgz",
- "integrity": "sha512-WDKCK7wLrvp49FXcql8iZHC2wSgPbp3bk5w5yc1LgZpLmk+ilPoquHPT+/kYM/ctxkQFwcrO78KiKMMEfz5Ytg==",
+ "version": "5.10.14",
+ "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-5.10.14.tgz",
+ "integrity": "sha512-8ZVIxte3GGQyhBZGSFHF8ieZ3CO8lT4XADi37lz1/yjwbA09/1BcehqXQpA3P0CD+jnKx2WiDpMQIasU4SiqrA==",
"requires": {
"tslib": "^1.7.1"
}
},
"@ngx-formly/ionic": {
- "version": "5.10.13",
- "resolved": "https://registry.npmjs.org/@ngx-formly/ionic/-/ionic-5.10.13.tgz",
- "integrity": "sha512-vWZr+bXb8cuG5Ld/DZ33ZyVinoY/Ks2OGA0I/rlxYbkoi6kPvWWlfGqv0EolAtNPQFZPalBekIuG76WUtVMjaA==",
+ "version": "5.10.14",
+ "resolved": "https://registry.npmjs.org/@ngx-formly/ionic/-/ionic-5.10.14.tgz",
+ "integrity": "sha512-goLD9f793BCmWDONjgV5XBZWczO6yM9xzUvvw9WsZXNj304RZDSacCjkzk3yuGxhhXTNhR3K7gBChRu0+dVdEQ==",
"requires": {
"tslib": "^1.9.0"
}
@@ -4384,15 +4384,6 @@
"tweetnacl": "^0.14.3"
}
},
- "better-assert": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
- "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
- "dev": true,
- "requires": {
- "callsite": "1.0.0"
- }
- },
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -4828,12 +4819,6 @@
"caller-callsite": "^2.0.0"
}
},
- "callsite": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
- "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
- "dev": true
- },
"callsites": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
@@ -5642,9 +5627,9 @@
}
},
"core-js": {
- "version": "3.8.2",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.2.tgz",
- "integrity": "sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A=="
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.9.0.tgz",
+ "integrity": "sha512-PyFBJaLq93FlyYdsndE5VaueA9K5cNB7CGzeCj191YYLhkQM0gdZR2SKihM70oF0wdqKSKClv/tEBOpoRmdOVQ=="
},
"core-js-compat": {
"version": "3.8.3",
@@ -6851,37 +6836,37 @@
}
},
"engine.io": {
- "version": "3.4.2",
- "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz",
- "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==",
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz",
+ "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==",
"dev": true,
"requires": {
"accepts": "~1.3.4",
"base64id": "2.0.0",
- "cookie": "0.3.1",
+ "cookie": "~0.4.1",
"debug": "~4.1.0",
"engine.io-parser": "~2.2.0",
- "ws": "^7.1.2"
+ "ws": "~7.4.2"
},
"dependencies": {
"cookie": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
- "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"dev": true
},
"ws": {
- "version": "7.3.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
- "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==",
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz",
+ "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==",
"dev": true
}
}
},
"engine.io-client": {
- "version": "3.4.4",
- "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz",
- "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==",
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.0.tgz",
+ "integrity": "sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==",
"dev": true,
"requires": {
"component-emitter": "~1.3.0",
@@ -6892,7 +6877,7 @@
"indexof": "0.0.1",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
- "ws": "~6.1.0",
+ "ws": "~7.4.2",
"xmlhttprequest-ssl": "~1.5.4",
"yeast": "0.1.2"
},
@@ -6912,26 +6897,11 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
- "parseqs": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
- "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==",
- "dev": true
- },
- "parseuri": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
- "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==",
- "dev": true
- },
"ws": {
- "version": "6.1.4",
- "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
- "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
- "dev": true,
- "requires": {
- "async-limiter": "~1.0.0"
- }
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz",
+ "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==",
+ "dev": true
}
}
},
@@ -9844,7 +9814,7 @@
},
"load-json-file": {
"version": "1.1.0",
- "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
"requires": {
"graceful-fs": "^4.1.2",
@@ -11131,12 +11101,6 @@
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
- "object-component": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
- "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=",
- "dev": true
- },
"object-copy": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
@@ -11727,22 +11691,16 @@
}
},
"parseqs": {
- "version": "0.0.5",
- "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
- "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
- "dev": true,
- "requires": {
- "better-assert": "~1.0.0"
- }
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
+ "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==",
+ "dev": true
},
"parseuri": {
- "version": "0.0.5",
- "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
- "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
- "dev": true,
- "requires": {
- "better-assert": "~1.0.0"
- }
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
+ "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==",
+ "dev": true
},
"parseurl": {
"version": "1.3.3",
@@ -13641,9 +13599,9 @@
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
},
"rxjs": {
- "version": "6.6.3",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz",
- "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==",
+ "version": "6.6.6",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz",
+ "integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==",
"requires": {
"tslib": "^1.9.0"
}
@@ -14182,16 +14140,16 @@
}
},
"socket.io": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz",
- "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==",
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.1.tgz",
+ "integrity": "sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==",
"dev": true,
"requires": {
"debug": "~4.1.0",
- "engine.io": "~3.4.0",
+ "engine.io": "~3.5.0",
"has-binary2": "~1.0.2",
"socket.io-adapter": "~1.1.0",
- "socket.io-client": "2.3.0",
+ "socket.io-client": "2.4.0",
"socket.io-parser": "~3.4.0"
}
},
@@ -14202,38 +14160,32 @@
"dev": true
},
"socket.io-client": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz",
- "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz",
+ "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==",
"dev": true,
"requires": {
"backo2": "1.0.2",
- "base64-arraybuffer": "0.1.5",
"component-bind": "1.0.0",
- "component-emitter": "1.2.1",
- "debug": "~4.1.0",
- "engine.io-client": "~3.4.0",
+ "component-emitter": "~1.3.0",
+ "debug": "~3.1.0",
+ "engine.io-client": "~3.5.0",
"has-binary2": "~1.0.2",
- "has-cors": "1.1.0",
"indexof": "0.0.1",
- "object-component": "0.0.3",
- "parseqs": "0.0.5",
- "parseuri": "0.0.5",
+ "parseqs": "0.0.6",
+ "parseuri": "0.0.6",
"socket.io-parser": "~3.3.0",
"to-array": "0.1.4"
},
"dependencies": {
- "base64-arraybuffer": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
- "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
- "dev": true
- },
- "component-emitter": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
- "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
- "dev": true
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
},
"isarray": {
"version": "2.0.1",
@@ -14248,31 +14200,14 @@
"dev": true
},
"socket.io-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz",
- "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==",
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz",
+ "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==",
"dev": true,
"requires": {
"component-emitter": "~1.3.0",
"debug": "~3.1.0",
"isarray": "2.0.1"
- },
- "dependencies": {
- "component-emitter": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
- "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
- "dev": true
- },
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- }
}
}
}
diff --git a/ui/package.json b/ui/package.json
index 40a51583faf..9702a6f10af 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "openems-ui",
- "version": "2021.3.0",
+ "version": "2021.4.0",
"author": "OpenEMS Association e.V.",
"homepage": "http://openems.io",
"scripts": {
@@ -23,9 +23,9 @@
"@angular/platform-browser-dynamic": "^10.2.4",
"@angular/router": "^10.2.4",
"@angular/service-worker": "^10.2.4",
- "@ionic-native/core": "^5.30.0",
- "@ionic-native/splash-screen": "^5.30.0",
- "@ionic-native/status-bar": "^5.30.0",
+ "@ionic-native/core": "^5.31.1",
+ "@ionic-native/splash-screen": "^5.31.1",
+ "@ionic-native/status-bar": "^5.31.1",
"@ionic/angular": "^5.5.4",
"@ngx-formly/core": "^5.10.13",
"@ngx-formly/ionic": "^5.10.13",
@@ -34,7 +34,7 @@
"angular2-uuid": "^1.1.1",
"chart.js": "^2.9.4",
"classlist.js": "^1.1.20150312",
- "core-js": "^3.8.2",
+ "core-js": "^3.9.0",
"d3": "5.15.0",
"date-fns": "^2.17.0",
"file-saver": "^2.0.5",
@@ -44,7 +44,7 @@
"ngx-spinner": "^10.0.1",
"node-sass": "^4.14.1",
"roboto-fontface": "0.10.0",
- "rxjs": "^6.6.3",
+ "rxjs": "^6.6.6",
"semver-compare-multi": "^1.0.3",
"tslib": "^1.14.1",
"zone.js": "^0.10.3"
diff --git a/ui/src/app/about/about.component.html b/ui/src/app/about/about.component.html
index 69beb34db08..bda39c0d3fc 100644
--- a/ui/src/app/about/about.component.html
+++ b/ui/src/app/about/about.component.html
@@ -19,8 +19,8 @@
About.openEMS
diff --git a/ui/src/app/edge/history/abstracthistorychart.ts b/ui/src/app/edge/history/abstracthistorychart.ts index e247e9266b0..4baf315706d 100644 --- a/ui/src/app/edge/history/abstracthistorychart.ts +++ b/ui/src/app/edge/history/abstracthistorychart.ts @@ -1,13 +1,13 @@ -import { ChannelAddress, Edge, EdgeConfig, Service, Utils } from "../../shared/shared"; +import { TranslateService } from '@ngx-translate/core'; import { ChartDataSets } from 'chart.js'; -import { ChartOptions, DEFAULT_TIME_CHART_OPTIONS, EMPTY_DATASET } from './shared'; import { differenceInDays } from 'date-fns'; +import { queryHistoricTimeseriesEnergyPerPeriodRequest } from 'src/app/shared/jsonrpc/request/queryHistoricTimeseriesEnergyPerPeriodRequest'; +import { queryHistoricTimeseriesEnergyPerPeriodResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse'; import { JsonrpcResponseError } from "../../shared/jsonrpc/base"; import { QueryHistoricTimeseriesDataRequest } from "../../shared/jsonrpc/request/queryHistoricTimeseriesDataRequest"; import { QueryHistoricTimeseriesDataResponse } from "../../shared/jsonrpc/response/queryHistoricTimeseriesDataResponse"; -import { queryHistoricTimeseriesEnergyPerPeriodRequest } from 'src/app/shared/jsonrpc/request/queryHistoricTimeseriesEnergyPerPeriodRequest'; -import { queryHistoricTimeseriesEnergyPerPeriodResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse'; -import { TranslateService } from '@ngx-translate/core'; +import { ChannelAddress, Edge, EdgeConfig, Service, Utils } from "../../shared/shared"; +import { ChartOptions, DEFAULT_TIME_CHART_OPTIONS, EMPTY_DATASET } from './shared'; // NOTE: Auto-refresh of widgets is currently disabled to reduce server load export abstract class AbstractHistoryChart { diff --git a/ui/src/app/edge/history/energy/energy.component.ts b/ui/src/app/edge/history/energy/energy.component.ts index 03c32218c8d..8d422158d03 100644 --- a/ui/src/app/edge/history/energy/energy.component.ts +++ b/ui/src/app/edge/history/energy/energy.component.ts @@ -1,25 +1,25 @@ -import { AbstractHistoryChart } from '../abstracthistorychart'; +import { formatNumber } from '@angular/common'; +import { Component, Input, OnChanges } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { addDays } from 'date-fns/esm'; -import { Base64PayloadResponse } from 'src/app/shared/jsonrpc/response/base64PayloadResponse'; -import { ChannelAddress, Edge, EdgeConfig, Service, Utils, Websocket } from '../../../shared/shared'; +import { ModalController, Platform } from '@ionic/angular'; +import { TranslateService } from '@ngx-translate/core'; import { ChartData, ChartDataSets, ChartLegendLabelItem, ChartTooltipItem } from 'chart.js'; -import { ChartOptions, Data, DEFAULT_TIME_CHART_OPTIONS, TooltipItem } from './../shared'; -import { Component, Input, OnChanges } from '@angular/core'; -import { DefaultTypes } from 'src/app/shared/service/defaulttypes'; import { differenceInDays, format, isSameDay, isSameMonth, isSameYear } from 'date-fns'; -import { EnergyModalComponent } from './modal/modal.component'; -import { formatNumber } from '@angular/common'; -import { ModalController, Platform } from '@ionic/angular'; -import { QueryHistoricTimeseriesDataResponse } from '../../../shared/jsonrpc/response/queryHistoricTimeseriesDataResponse'; -import { queryHistoricTimeseriesEnergyPerPeriodResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse'; -import { QueryHistoricTimeseriesEnergyResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse'; -import { QueryHistoricTimeseriesExportXlxsRequest } from 'src/app/shared/jsonrpc/request/queryHistoricTimeseriesExportXlxs'; +import { addDays } from 'date-fns/esm'; +import * as FileSaver from 'file-saver'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { TranslateService } from '@ngx-translate/core'; +import { QueryHistoricTimeseriesExportXlxsRequest } from 'src/app/shared/jsonrpc/request/queryHistoricTimeseriesExportXlxs'; +import { Base64PayloadResponse } from 'src/app/shared/jsonrpc/response/base64PayloadResponse'; +import { queryHistoricTimeseriesEnergyPerPeriodResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse'; +import { QueryHistoricTimeseriesEnergyResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse'; import { UnitvaluePipe } from 'src/app/shared/pipe/unitvalue/unitvalue.pipe'; -import * as FileSaver from 'file-saver'; +import { DefaultTypes } from 'src/app/shared/service/defaulttypes'; +import { QueryHistoricTimeseriesDataResponse } from '../../../shared/jsonrpc/response/queryHistoricTimeseriesDataResponse'; +import { ChannelAddress, Edge, EdgeConfig, Service, Utils, Websocket } from '../../../shared/shared'; +import { AbstractHistoryChart } from '../abstracthistorychart'; +import { ChartOptions, Data, DEFAULT_TIME_CHART_OPTIONS, TooltipItem } from './../shared'; +import { EnergyModalComponent } from './modal/modal.component'; type EnergyChartLabels = { production: string, diff --git a/ui/src/app/shared/jsonrpc/request/get24HoursPredictionRequest.ts b/ui/src/app/shared/jsonrpc/request/get24HoursPredictionRequest.ts new file mode 100644 index 00000000000..30ac302597c --- /dev/null +++ b/ui/src/app/shared/jsonrpc/request/get24HoursPredictionRequest.ts @@ -0,0 +1,34 @@ +import { ChannelAddress } from "../../../shared/type/channeladdress"; +import { format } from 'date-fns'; +import { JsonrpcRequest } from "../base"; +import { JsonRpcUtils } from "../jsonrpcutils"; + +/** + * Represents a JSON-RPC Request to query a 24 Hours Prediction. + * + *
+ * { + * "jsonrpc": "2.0", + * "id": UUID, + * "method": "get24HoursPredictionRequest", + * "params": { + * "channels": ChannelAddress[] + * } + * } + *+ */ +export class Get24HoursPredictionRequest extends JsonrpcRequest { + + static METHOD: string = "get24HoursPrediction"; + + public constructor( + private channels: ChannelAddress[] + ) { + super(Get24HoursPredictionRequest.METHOD, { + channels: JsonRpcUtils.channelsToStringArray(channels) + }); + // delete local fields, otherwise they are sent with the JSON-RPC Request + delete this.channels; + } + +} \ No newline at end of file diff --git a/ui/src/app/shared/jsonrpc/response/get24HoursPredictionResponse.ts b/ui/src/app/shared/jsonrpc/response/get24HoursPredictionResponse.ts new file mode 100644 index 00000000000..489e0512c21 --- /dev/null +++ b/ui/src/app/shared/jsonrpc/response/get24HoursPredictionResponse.ts @@ -0,0 +1,30 @@ +import { JsonrpcResponseSuccess } from "../base"; + +export class Prediction { + [channelAddress: string]: number[] +} + +/** + * Wraps a JSON-RPC Response for a Get24HoursPredictionRequest. + * + *
+ * { + * "jsonrpc": "2.0", + * "id": UUID, + * "result": { + * "componentId/channelId": [ + * value1, value2,... + * ] + * } + * } + *+ */ +export class Get24HoursPredictionResponse extends JsonrpcResponseSuccess { + + public constructor( + public readonly id: string, + public readonly result: Prediction + ) { + super(id, result); + } +} \ No newline at end of file